summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/browser/menu2
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
commitda4c7e7ed675c3bf405668739c3012d140856109 (patch)
treecdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/android-components/components/browser/menu2
parentAdding upstream version 125.0.3. (diff)
downloadfirefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz
firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/browser/menu2')
-rw-r--r--mobile/android/android-components/components/browser/menu2/README.md51
-rw-r--r--mobile/android/android-components/components/browser/menu2/build.gradle50
-rw-r--r--mobile/android/android-components/components/browser/menu2/lint.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt182
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt81
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt47
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt25
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt33
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt80
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt53
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt55
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt60
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt55
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt224
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt51
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt71
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt62
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt261
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt95
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt118
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt95
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.xml22
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml21
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml39
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml36
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml18
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml8
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml29
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml15
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml13
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml31
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml5
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml7
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml8
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml48
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml10
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml88
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt132
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt99
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt61
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt24
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt75
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt103
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt130
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt117
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt177
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt86
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt68
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt856
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt143
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt120
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt92
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker3
-rw-r--r--mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties1
181 files changed, 5531 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/browser/menu2/README.md b/mobile/android/android-components/components/browser/menu2/README.md
new file mode 100644
index 0000000000..dc1b129b65
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/README.md
@@ -0,0 +1,51 @@
+# [Android Components](../../../README.md) > Browser > Menu 2
+
+A generic menu with customizable items primarily for browser toolbars.
+
+This replaces the [browser-menu](../menu) component with a new API using immutable objects,
+designed to work well with [lib-state](../../lib/state).
+
+## Usage
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
+
+```Groovy
+implementation "org.mozilla.components:browser-menu2:{latest-version}"
+```
+
+### MenuController
+The menu controller is used to control the items in the menu as well as displaying the menu popup.
+
+Sample code can be found in [Sample Toolbar app](https://github.com/mozilla-mobile/android-components/tree/main/samples/toolbar).
+
+There are multiple properties that you customize of the browser menu by just adding them into your dimens.xml file.
+
+```xml
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <!--Change how rounded the corners of the menu should be-->
+ <dimen name="mozac_browser_menu2_corner_radius" tools:ignore="UnusedResources">4dp</dimen>
+
+ <!--Change how much shadow the menu should have-->
+ <dimen name="mozac_browser_menu2_elevation" tools:ignore="UnusedResources">4dp</dimen>
+
+ <!--Change the width of the menu - can also be set in MenuController#show()-->
+ <dimen name="mozac_browser_menu2_width" tools:ignore="UnusedResources">250dp</dimen>
+
+ <!--Change the top and bottom padding of the menu-->
+ <dimen name="mozac_browser_menu2_padding_vertical" tools:ignore="UnusedResources">8dp</dimen>
+
+</resources>
+```
+
+Options displayed in the menu are configured by using a list of `MenuCandidate` objects.
+The list of options can be sent to the menu by calling `MenuController#submitList()`.
+To change the displayed options, simply call `submitList` again with a new list.
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/components/browser/menu2/build.gradle b/mobile/android/android-components/components/browser/menu2/build.gradle
new file mode 100644
index 0000000000..ccb4e0bdd8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/build.gradle
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.browser.menu2'
+}
+
+dependencies {
+ implementation project(':concept-menu')
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_appcompat
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_recyclerview
+ implementation ComponentsDependencies.androidx_cardview
+ implementation ComponentsDependencies.androidx_constraintlayout
+ implementation ComponentsDependencies.androidx_coordinatorlayout
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/browser/menu2/lint.xml b/mobile/android/android-components/components/browser/menu2/lint.xml
new file mode 100644
index 0000000000..81bcc3bfb8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/lint.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<lint>
+ <issue id="Overdraw" severity="ignore" />
+</lint> \ No newline at end of file
diff --git a/mobile/android/android-components/components/browser/menu2/proguard-rules.pro b/mobile/android/android-components/components/browser/menu2/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/menu2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1eccdee26a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application android:supportsRtl="true" />
+</manifest>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt
new file mode 100644
index 0000000000..eaf1998661
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2
+
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.PopupWindow
+import androidx.annotation.VisibleForTesting
+import androidx.core.widget.PopupWindowCompat
+import mozilla.components.browser.menu2.ext.MenuPositioningData
+import mozilla.components.browser.menu2.ext.inferMenuPositioningData
+import mozilla.components.browser.menu2.view.MenuView
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Orientation
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.ext.findNestedMenuCandidate
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+
+/**
+ * Controls a popup menu composed of MenuCandidate objects.
+ * @param visibleSide Sets the menu to open with either the start or end visible.
+ * @param style Custom styling for this menu controller.
+ */
+class BrowserMenuController(
+ private val visibleSide: Side = Side.START,
+ private val style: MenuStyle? = null,
+) : MenuController, Observable<MenuController.Observer> by ObserverRegistry() {
+
+ private var currentPopupInfo: PopupMenuInfo? = null
+ private var menuCandidates: List<MenuCandidate> = emptyList()
+
+ private val menuDismissListener = PopupWindow.OnDismissListener {
+ currentPopupInfo = null
+ notifyObservers { onDismiss() }
+ }
+
+ override fun show(
+ anchor: View,
+ orientation: Orientation?,
+ autoDismiss: Boolean,
+ ): PopupWindow {
+ // If the menu is already displayed do not display it again.
+ currentPopupInfo?.window?.let {
+ return it
+ }
+
+ val view = MenuView(anchor.context).apply {
+ // Show nested list if present, or the standard menu candidates list.
+ submitList(menuCandidates)
+ setVisibleSide(visibleSide)
+ style?.let { setStyle(it) }
+ }
+
+ if (autoDismiss) {
+ // Monitor for changes to the parent layout and dismiss pop up if displayed, else the menu
+ // could be displayed in the wrong position. For example, if the menu is displayed and the
+ // device orientation changes from portrait to landscape and vice versa.
+ anchor.rootView.addOnLayoutChangeListener { _, _, _, right, bottom, _, _, oldRight, oldBottom ->
+ if (bottom != oldBottom || right != oldRight) {
+ dismiss()
+ }
+ }
+ }
+
+ return MenuPopupWindow(view).apply {
+ view.onDismiss = ::dismiss
+ view.onReopenMenu = ::reopenMenu
+ setOnDismissListener(menuDismissListener)
+ inferMenuPositioningData(
+ containerView = view,
+ anchor = anchor,
+ style = style,
+ orientation = orientation,
+ )?.let {
+ displayPopup(it)
+ }
+ }.also {
+ currentPopupInfo = PopupMenuInfo(
+ window = it,
+ anchor = anchor,
+ orientation = orientation,
+ nested = null,
+ )
+ }
+ }
+
+ /**
+ * Re-opens the menu and displays the given nested list.
+ * No-op if the menu is not yet open.
+ */
+ private fun reopenMenu(nested: NestedMenuCandidate?) {
+ val info = currentPopupInfo ?: return
+ info.window.run {
+ // Dismiss silently
+ setOnDismissListener(null)
+ dismiss()
+ setOnDismissListener(menuDismissListener)
+
+ // Quickly remove the current list
+ view.submitList(null)
+ // Display the new nested list
+ view.submitList(nested?.subMenuItems ?: menuCandidates)
+ // Attempt tp reopen the menu
+ inferMenuPositioningData(
+ containerView = view,
+ anchor = info.anchor,
+ style = style,
+ orientation = info.orientation,
+ )?.let {
+ displayPopup(it)
+ }
+ }
+ currentPopupInfo = info.copy(nested = nested)
+ }
+
+ /**
+ * Dismiss the menu popup if the menu is visible.
+ */
+ override fun dismiss() {
+ currentPopupInfo?.window?.dismiss()
+ }
+
+ /**
+ * Changes the contents of the menu.
+ */
+ override fun submitList(list: List<MenuCandidate>) {
+ menuCandidates = list
+ val info = currentPopupInfo
+
+ // If menu is already open, update the displayed items
+ if (info != null) {
+ // If a nested menu is open, it should be displayed
+ val displayedItems = if (info.nested != null) {
+ list.findNestedMenuCandidate(info.nested.id)?.subMenuItems
+ } else {
+ list
+ }
+
+ // If the new menu is null, close & reopen the popup on the main list
+ if (displayedItems == null) {
+ // close & reopen popup
+ reopenMenu(nested = null)
+ } else {
+ info.window.view.submitList(displayedItems)
+ }
+ }
+
+ notifyObservers { onMenuListSubmit(list) }
+ }
+
+ private class MenuPopupWindow(
+ val view: MenuView,
+ ) : PopupWindow(view, WRAP_CONTENT, WRAP_CONTENT, true)
+
+ private data class PopupMenuInfo(
+ val window: MenuPopupWindow,
+ val anchor: View,
+ val orientation: Orientation?,
+ val nested: NestedMenuCandidate? = null,
+ )
+}
+
+/**
+ * Show a [PopupWindow] given the positioning data.
+ */
+@VisibleForTesting
+internal fun PopupWindow.displayPopup(positioningData: MenuPositioningData) {
+ inputMethodMode = PopupWindow.INPUT_METHOD_NOT_NEEDED
+
+ animationStyle = positioningData.animation
+ height = positioningData.containerHeight
+
+ PopupWindowCompat.setOverlapAnchor(this, true)
+ showAtLocation(positioningData.anchor, Gravity.NO_GRAVITY, positioningData.x, positioningData.y)
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt
new file mode 100644
index 0000000000..eb592656c2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.CompoundButton
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.icons.MenuIconAdapter
+import mozilla.components.browser.menu2.ext.applyBackgroundEffect
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate.ButtonType
+
+internal abstract class CompoundMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ private val dismiss: () -> Unit,
+) : MenuCandidateViewHolder<CompoundMenuCandidate>(itemView, inflater), CompoundButton.OnCheckedChangeListener {
+
+ private val layout = itemView as ConstraintLayout
+ private val compoundButton: CompoundButton = itemView.findViewById(R.id.label)
+ private val startIcon = MenuIconAdapter(layout, inflater, Side.START, dismiss)
+ private var onCheckedChangeListener: ((Boolean) -> Unit)? = null
+
+ override fun bind(newCandidate: CompoundMenuCandidate, oldCandidate: CompoundMenuCandidate?) {
+ super.bind(newCandidate, oldCandidate)
+ onCheckedChangeListener = newCandidate.onCheckedChange
+ compoundButton.text = newCandidate.text
+ startIcon.bind(newCandidate.start, oldCandidate?.start)
+ compoundButton.applyStyle(newCandidate.textStyle, oldCandidate?.textStyle)
+ itemView.applyBackgroundEffect(newCandidate.effect, oldCandidate?.effect)
+
+ // isChecked calls the listener automatically
+ compoundButton.setOnCheckedChangeListener(null)
+ compoundButton.isChecked = newCandidate.isChecked
+ compoundButton.setOnCheckedChangeListener(this)
+ }
+
+ override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
+ onCheckedChangeListener?.invoke(isChecked)
+ dismiss()
+ }
+
+ companion object {
+ @LayoutRes
+ fun getLayoutResource(candidate: CompoundMenuCandidate) = when (candidate.end) {
+ ButtonType.CHECKBOX -> CompoundCheckboxMenuCandidateViewHolder.layoutResource
+ ButtonType.SWITCH -> CompoundSwitchMenuCandidateViewHolder.layoutResource
+ }
+ }
+}
+
+internal class CompoundCheckboxMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ dismiss: () -> Unit,
+) : CompoundMenuCandidateViewHolder(itemView, inflater, dismiss) {
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_compound_checkbox
+ }
+}
+
+internal class CompoundSwitchMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ dismiss: () -> Unit,
+) : CompoundMenuCandidateViewHolder(itemView, inflater, dismiss) {
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_compound_switch
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..84ea1b89d6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.core.view.updateLayoutParams
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+
+internal class DecorativeTextMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+) : MenuCandidateViewHolder<DecorativeTextMenuCandidate>(itemView, inflater) {
+
+ private val textView: TextView get() = itemView as TextView
+
+ override fun bind(
+ newCandidate: DecorativeTextMenuCandidate,
+ oldCandidate: DecorativeTextMenuCandidate?,
+ ) {
+ super.bind(newCandidate, oldCandidate)
+
+ textView.text = newCandidate.text
+ textView.applyStyle(newCandidate.textStyle, oldCandidate?.textStyle)
+ applyHeight(newCandidate.height, oldCandidate?.height)
+ }
+
+ private fun applyHeight(newHeight: Int?, oldHeight: Int?) {
+ if (newHeight != oldHeight) {
+ textView.updateLayoutParams {
+ height = newHeight ?: itemView.resources
+ .getDimensionPixelSize(R.dimen.mozac_browser_menu2_candidate_container_layout_height)
+ }
+ }
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_decorative_text
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..aad9ecffe7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import androidx.annotation.LayoutRes
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+
+/**
+ * View holder that displays a divider.
+ */
+internal class DividerMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+) : MenuCandidateViewHolder<DividerMenuCandidate>(itemView, inflater) {
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_divider
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt
new file mode 100644
index 0000000000..3ea96d2418
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * View holders that extend this base class are passed both the new value and last value
+ * when [bind] is called. Use this information to diff the changes between the two values.
+ */
+internal abstract class LastItemViewHolder<T>(
+ itemView: View,
+) : RecyclerView.ViewHolder(itemView) {
+
+ protected var lastCandidate: T? = null
+
+ /**
+ * Updates the held view to reflect changes in the menu option.
+ *
+ * @param newCandidate New value to use.
+ * @param oldCandidate Previously set value.
+ * If this is the first time [bind] was called, null is passed.
+ */
+ protected abstract fun bind(newCandidate: T, oldCandidate: T?)
+
+ fun bind(option: T) {
+ bind(option, lastCandidate)
+ lastCandidate = option
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt
new file mode 100644
index 0000000000..bc39865177
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+internal class MenuCandidateListAdapter(
+ private val inflater: LayoutInflater,
+ private val dismiss: () -> Unit,
+ private val reopenMenu: (NestedMenuCandidate) -> Unit,
+) : ListAdapter<MenuCandidate, MenuCandidateViewHolder<out MenuCandidate>>(MenuCandidateDiffer) {
+
+ @LayoutRes
+ override fun getItemViewType(position: Int) = when (val item = getItem(position)) {
+ is TextMenuCandidate -> TextMenuCandidateViewHolder.layoutResource
+ is DecorativeTextMenuCandidate -> DecorativeTextMenuCandidateViewHolder.layoutResource
+ is CompoundMenuCandidate -> CompoundMenuCandidateViewHolder.getLayoutResource(item)
+ is NestedMenuCandidate -> NestedMenuCandidateViewHolder.layoutResource
+ is RowMenuCandidate -> RowMenuCandidateViewHolder.layoutResource
+ is DividerMenuCandidate -> DividerMenuCandidateViewHolder.layoutResource
+ }
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ @LayoutRes viewType: Int,
+ ): MenuCandidateViewHolder<out MenuCandidate> {
+ val view = inflater.inflate(viewType, parent, false)
+ return when (viewType) {
+ TextMenuCandidateViewHolder.layoutResource ->
+ TextMenuCandidateViewHolder(view, inflater, dismiss)
+ DecorativeTextMenuCandidateViewHolder.layoutResource ->
+ DecorativeTextMenuCandidateViewHolder(view, inflater)
+ CompoundCheckboxMenuCandidateViewHolder.layoutResource ->
+ CompoundCheckboxMenuCandidateViewHolder(view, inflater, dismiss)
+ CompoundSwitchMenuCandidateViewHolder.layoutResource ->
+ CompoundSwitchMenuCandidateViewHolder(view, inflater, dismiss)
+ NestedMenuCandidateViewHolder.layoutResource ->
+ NestedMenuCandidateViewHolder(view, inflater, dismiss, reopenMenu)
+ RowMenuCandidateViewHolder.layoutResource ->
+ RowMenuCandidateViewHolder(view, inflater, dismiss)
+ DividerMenuCandidateViewHolder.layoutResource ->
+ DividerMenuCandidateViewHolder(view, inflater)
+ else -> throw IllegalArgumentException("Invalid viewType $viewType")
+ }
+ }
+
+ override fun onBindViewHolder(holder: MenuCandidateViewHolder<out MenuCandidate>, position: Int) {
+ val item = getItem(position)
+ when (holder) {
+ is TextMenuCandidateViewHolder -> holder.bind(item as TextMenuCandidate)
+ is DecorativeTextMenuCandidateViewHolder -> holder.bind(item as DecorativeTextMenuCandidate)
+ is CompoundMenuCandidateViewHolder -> holder.bind(item as CompoundMenuCandidate)
+ is NestedMenuCandidateViewHolder -> holder.bind(item as NestedMenuCandidate)
+ is RowMenuCandidateViewHolder -> holder.bind(item as RowMenuCandidate)
+ is DividerMenuCandidateViewHolder -> holder.bind(item as DividerMenuCandidate)
+ }
+ }
+}
+
+private object MenuCandidateDiffer : DiffUtil.ItemCallback<MenuCandidate>() {
+ override fun areItemsTheSame(oldItem: MenuCandidate, newItem: MenuCandidate) =
+ oldItem::class == newItem::class
+
+ @Suppress("DiffUtilEquals")
+ override fun areContentsTheSame(oldItem: MenuCandidate, newItem: MenuCandidate) =
+ oldItem == newItem
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..98212b9b36
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import androidx.annotation.CallSuper
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.candidate.MenuCandidate
+
+internal abstract class MenuCandidateViewHolder<T : MenuCandidate>(
+ itemView: View,
+ protected val inflater: LayoutInflater,
+) : LastItemViewHolder<T>(itemView) {
+
+ @CallSuper
+ override fun bind(newCandidate: T, oldCandidate: T?) {
+ itemView.applyStyle(newCandidate.containerStyle, oldCandidate?.containerStyle)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..c899f5e17f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.icons.MenuIconAdapter
+import mozilla.components.browser.menu2.ext.applyBackgroundEffect
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+
+internal class NestedMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ dismiss: () -> Unit,
+ private val reopenMenu: (NestedMenuCandidate) -> Unit,
+) : MenuCandidateViewHolder<NestedMenuCandidate>(itemView, inflater), View.OnClickListener {
+
+ private val layout = itemView as ConstraintLayout
+ private val textView: TextView get() = itemView.findViewById(R.id.label)
+ private val startIcon = MenuIconAdapter(layout, inflater, Side.START, dismiss)
+ private val endIcon = MenuIconAdapter(layout, inflater, Side.END, dismiss)
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ override fun bind(newCandidate: NestedMenuCandidate, oldCandidate: NestedMenuCandidate?) {
+ super.bind(newCandidate, oldCandidate)
+
+ textView.text = newCandidate.text
+ textView.applyStyle(newCandidate.textStyle, oldCandidate?.textStyle)
+ itemView.applyBackgroundEffect(newCandidate.effect, oldCandidate?.effect)
+ startIcon.bind(newCandidate.start, oldCandidate?.start)
+ endIcon.bind(newCandidate.end, oldCandidate?.end)
+ }
+
+ override fun onClick(v: View?) {
+ lastCandidate?.let { reopenMenu(it) }
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_nested
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..a2fb8d0b0e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import androidx.annotation.LayoutRes
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+
+/**
+ * Displays a row of small menu options.
+ */
+internal class RowMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ private val dismiss: () -> Unit,
+) : MenuCandidateViewHolder<RowMenuCandidate>(itemView, inflater) {
+
+ private val layout = itemView as LinearLayout
+ private var buttonViewHolders = emptyList<SmallMenuCandidateViewHolder>()
+
+ override fun bind(newCandidate: RowMenuCandidate, oldCandidate: RowMenuCandidate?) {
+ super.bind(newCandidate, oldCandidate)
+
+ // If the number of children in the row changes,
+ // build new holders for each of them.
+ if (newCandidate.items.size != oldCandidate?.items?.size) {
+ layout.removeAllViews()
+ // Create new view holders list
+ buttonViewHolders = newCandidate.items.map {
+ val button = inflater.inflate(
+ SmallMenuCandidateViewHolder.layoutResource,
+ layout,
+ false,
+ )
+ layout.addView(button)
+ SmallMenuCandidateViewHolder(button, dismiss)
+ }
+ }
+
+ // Use the button view holders to compare individual menu items in the row.
+ buttonViewHolders.zip(newCandidate.items).forEach { (viewHolder, item) ->
+ viewHolder.bind(item)
+ }
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_row
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..fb77f1b325
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.View
+import androidx.annotation.LayoutRes
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.appcompat.widget.TooltipCompat
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.ext.applyIcon
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+
+internal class SmallMenuCandidateViewHolder(
+ itemView: View,
+ private val dismiss: () -> Unit,
+) : LastItemViewHolder<SmallMenuCandidate>(itemView),
+ View.OnClickListener,
+ View.OnLongClickListener {
+
+ private val iconView = itemView as AppCompatImageButton
+ private var onClickListener: (() -> Unit)? = null
+ private var onLongClickListener: (() -> Boolean)? = null
+
+ init {
+ iconView.setOnClickListener(this)
+ iconView.setOnLongClickListener(this)
+ iconView.isLongClickable = false
+ }
+
+ override fun bind(newCandidate: SmallMenuCandidate, oldCandidate: SmallMenuCandidate?) {
+ if (newCandidate.contentDescription != oldCandidate?.contentDescription) {
+ iconView.contentDescription = newCandidate.contentDescription
+ TooltipCompat.setTooltipText(iconView, newCandidate.contentDescription)
+ }
+ onClickListener = newCandidate.onClick
+ onLongClickListener = newCandidate.onLongClick
+ iconView.isLongClickable = newCandidate.onLongClick != null
+ iconView.applyIcon(newCandidate.icon, oldCandidate?.icon)
+ iconView.applyStyle(newCandidate.containerStyle, oldCandidate?.containerStyle)
+ }
+
+ override fun onClick(v: View?) {
+ onClickListener?.invoke()
+ dismiss()
+ }
+
+ override fun onLongClick(v: View?): Boolean {
+ val result = onLongClickListener?.invoke() ?: false
+ dismiss()
+ return result
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_row_small_icon
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt
new file mode 100644
index 0000000000..f9fcf1839d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.icons.MenuIconAdapter
+import mozilla.components.browser.menu2.ext.applyBackgroundEffect
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+
+internal class TextMenuCandidateViewHolder(
+ itemView: View,
+ inflater: LayoutInflater,
+ private val dismiss: () -> Unit,
+) : MenuCandidateViewHolder<TextMenuCandidate>(itemView, inflater), View.OnClickListener {
+
+ private val layout = itemView as ConstraintLayout
+ private val textView: TextView get() = itemView.findViewById(R.id.label)
+ private val startIcon = MenuIconAdapter(layout, inflater, Side.START, dismiss)
+ private val endIcon = MenuIconAdapter(layout, inflater, Side.END, dismiss)
+ private var onClickListener: (() -> Unit)? = null
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ override fun bind(newCandidate: TextMenuCandidate, oldCandidate: TextMenuCandidate?) {
+ super.bind(newCandidate, oldCandidate)
+
+ textView.text = newCandidate.text
+ textView.applyStyle(newCandidate.textStyle, oldCandidate?.textStyle)
+ onClickListener = newCandidate.onClick
+ itemView.applyBackgroundEffect(newCandidate.effect, oldCandidate?.effect)
+ startIcon.bind(newCandidate.start, oldCandidate?.start)
+ endIcon.bind(newCandidate.end, oldCandidate?.end)
+ }
+
+ override fun onClick(v: View?) {
+ onClickListener?.invoke()
+ dismiss()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_candidate_text
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt
new file mode 100644
index 0000000000..4ffc031a20
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter.icons
+
+import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageButton
+import android.widget.ImageView
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.core.view.isVisible
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.ext.applyIcon
+import mozilla.components.browser.menu2.ext.applyNotificationEffect
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableButtonMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuIcon
+import mozilla.components.support.base.log.logger.Logger
+
+internal abstract class MenuIconWithDrawableViewHolder<T : MenuIcon>(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+) : MenuIconViewHolder<T>(parent, inflater) {
+
+ protected abstract val imageView: ImageView
+
+ protected fun setup(imageView: View, side: Side) {
+ updateConstraints {
+ connect(R.id.icon, TOP, PARENT_ID, TOP)
+ connect(R.id.icon, BOTTOM, PARENT_ID, BOTTOM)
+ val margin = parent.resources
+ .getDimensionPixelSize(R.dimen.mozac_browser_menu2_icon_padding_start)
+ when (side) {
+ Side.START -> {
+ connect(imageView.id, START, PARENT_ID, START)
+ connect(imageView.id, END, R.id.label, START, margin)
+ connect(R.id.label, START, imageView.id, END)
+ }
+ Side.END -> {
+ connect(imageView.id, END, PARENT_ID, END)
+ connect(imageView.id, START, R.id.label, END, margin)
+ connect(R.id.label, END, imageView.id, START)
+ }
+ }
+ }
+ }
+
+ override fun disconnect() {
+ parent.removeView(imageView)
+ super.disconnect()
+ }
+}
+
+internal class DrawableMenuIconViewHolder(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+ side: Side,
+) : MenuIconWithDrawableViewHolder<DrawableMenuIcon>(parent, inflater) {
+
+ override val imageView: ImageView = inflate(layoutResource).findViewById(R.id.icon)
+ private var effectView: ImageView? = null
+
+ init {
+ setup(imageView, side)
+ }
+
+ override fun bind(newIcon: DrawableMenuIcon, oldIcon: DrawableMenuIcon?) {
+ imageView.applyIcon(newIcon, oldIcon)
+
+ // Only inflate the effect container if needed
+ if (newIcon.effect != null) {
+ createEffectView().applyNotificationEffect(newIcon.effect as LowPriorityHighlightEffect, oldIcon?.effect)
+ } else {
+ effectView?.isVisible = false
+ }
+ }
+
+ private fun createEffectView(): ImageView {
+ if (effectView == null) {
+ val effect: ImageView = inflate(notificationDotLayoutResource).findViewById(R.id.notification_dot)
+ updateConstraints {
+ connect(effect.id, TOP, imageView.id, TOP)
+ connect(effect.id, END, imageView.id, END)
+ }
+ effectView = effect
+ }
+ return effectView!!
+ }
+
+ override fun disconnect() {
+ effectView?.let { parent.removeView(it) }
+ super.disconnect()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_icon_drawable
+ val notificationDotLayoutResource = R.layout.mozac_browser_menu2_icon_notification_dot
+ }
+}
+
+internal class DrawableButtonMenuIconViewHolder(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+ side: Side,
+ private val dismiss: () -> Unit,
+) : MenuIconWithDrawableViewHolder<DrawableButtonMenuIcon>(parent, inflater), View.OnClickListener {
+
+ override val imageView: ImageButton = inflate(layoutResource).findViewById(R.id.icon)
+ private var onClickListener: (() -> Unit)? = null
+
+ init {
+ setup(imageView, side)
+ imageView.setOnClickListener(this)
+ }
+
+ override fun bind(newIcon: DrawableButtonMenuIcon, oldIcon: DrawableButtonMenuIcon?) {
+ imageView.applyIcon(newIcon, oldIcon)
+ onClickListener = newIcon.onClick
+ }
+
+ override fun onClick(v: View?) {
+ onClickListener?.invoke()
+ dismiss()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_icon_button
+ }
+}
+
+internal class AsyncDrawableMenuIconViewHolder(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+ side: Side,
+ private val logger: Logger = Logger("mozac-menu2-AsyncDrawableMenuIconViewHolder"),
+) : MenuIconWithDrawableViewHolder<AsyncDrawableMenuIcon>(parent, inflater) {
+
+ private val scope = MainScope()
+ override val imageView: ImageView = inflate(layoutResource).findViewById(R.id.icon)
+ private var effectView: ImageView? = null
+ private var iconJob: Job? = null
+
+ init {
+ setup(imageView, side)
+ }
+
+ override fun bind(newIcon: AsyncDrawableMenuIcon, oldIcon: AsyncDrawableMenuIcon?) {
+ if (newIcon.loadDrawable != oldIcon?.loadDrawable) {
+ imageView.setImageDrawable(newIcon.loadingDrawable)
+ iconJob?.cancel()
+ iconJob = scope.launch { loadIcon(newIcon.loadDrawable, newIcon.fallbackDrawable) }
+ }
+
+ if (newIcon.tint != oldIcon?.tint) {
+ imageView.imageTintList = newIcon.tint?.let { ColorStateList.valueOf(it) }
+ }
+
+ // Only inflate the effect container if needed
+ if (newIcon.effect != null) {
+ createEffectView().applyNotificationEffect(newIcon.effect as LowPriorityHighlightEffect, oldIcon?.effect)
+ } else {
+ effectView?.isVisible = false
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ private suspend fun loadIcon(
+ loadDrawable: suspend (width: Int, height: Int) -> Drawable?,
+ fallback: Drawable?,
+ ) {
+ val drawable = try {
+ loadDrawable(imageView.measuredWidth, imageView.measuredHeight)
+ } catch (throwable: Throwable) {
+ logger.error(
+ message = "Failed to load browser action icon, falling back to default.",
+ throwable = throwable,
+ )
+ fallback
+ }
+ imageView.setImageDrawable(drawable)
+ }
+
+ private fun createEffectView(): ImageView {
+ if (effectView == null) {
+ val effect: ImageView = inflate(notificationDotLayoutResource).findViewById(R.id.notification_dot)
+ updateConstraints {
+ connect(effect.id, TOP, imageView.id, TOP)
+ connect(effect.id, END, imageView.id, END)
+ }
+ effectView = effect
+ }
+ return effectView!!
+ }
+
+ override fun disconnect() {
+ effectView?.let { parent.removeView(it) }
+ scope.cancel()
+ super.disconnect()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_icon_drawable
+ val notificationDotLayoutResource = R.layout.mozac_browser_menu2_icon_notification_dot
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt
new file mode 100644
index 0000000000..737796c682
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter.icons
+
+import android.view.LayoutInflater
+import androidx.annotation.VisibleForTesting
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableButtonMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.MenuIcon
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+
+/**
+ * Helper class to manage a [ConstraintLayout] that can contain menu icon views.
+ * Different holder classes are used to swap out the child views.
+ */
+internal class MenuIconAdapter(
+ private val parent: ConstraintLayout,
+ private val inflater: LayoutInflater,
+ private val side: Side,
+ private val dismiss: () -> Unit,
+) {
+
+ private var viewHolder: MenuIconViewHolder<out MenuIcon>? = null
+
+ fun bind(newIcon: MenuIcon?, oldIcon: MenuIcon?) {
+ if (newIcon == null && oldIcon != null) {
+ viewHolder?.disconnect()
+ viewHolder = null
+ } else if (newIcon != null) {
+ if (oldIcon == null || newIcon::class != oldIcon::class) {
+ viewHolder?.disconnect()
+ viewHolder = createViewHolder(newIcon)
+ }
+
+ viewHolder?.bindAndCast(newIcon, oldIcon)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createViewHolder(item: MenuIcon): MenuIconViewHolder<*> = when (item) {
+ is DrawableMenuIcon -> DrawableMenuIconViewHolder(parent, inflater, side)
+ is DrawableButtonMenuIcon -> DrawableButtonMenuIconViewHolder(parent, inflater, side, dismiss)
+ is AsyncDrawableMenuIcon -> AsyncDrawableMenuIconViewHolder(parent, inflater, side)
+ is TextMenuIcon -> TextMenuIconViewHolder(parent, inflater, side)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt
new file mode 100644
index 0000000000..8af37ce947
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter.icons
+
+import android.view.LayoutInflater
+import android.view.View
+import androidx.annotation.CallSuper
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.MenuIcon
+
+/**
+ * View holder with a [bind] method that passes the previously bound value.
+ */
+internal abstract class MenuIconViewHolder<T : MenuIcon>(
+ protected val parent: ConstraintLayout,
+ protected val inflater: LayoutInflater,
+) {
+
+ @Suppress("Unchecked_Cast")
+ fun bindAndCast(newIcon: MenuIcon, oldIcon: MenuIcon?) {
+ bind(newIcon as T, oldIcon as T?)
+ }
+
+ /**
+ * Updates the held view to reflect changes in the menu option icon.
+ *
+ * @param newIcon New values to use.
+ * @param oldIcon Previously set values.
+ */
+ protected abstract fun bind(newIcon: T, oldIcon: T?)
+
+ /**
+ * Inflates the layout resource and adds it to the parent layout.
+ */
+ protected fun inflate(@LayoutRes layoutResource: Int): View {
+ val view = inflater.inflate(layoutResource, parent, false)
+ parent.addView(view)
+ return view
+ }
+
+ /**
+ * Changes the constraints applied to [parent].
+ */
+ protected inline fun updateConstraints(update: ConstraintSet.() -> Unit) {
+ ConstraintSet().apply {
+ clone(parent)
+ update()
+ applyTo(parent)
+ }
+ }
+
+ /**
+ * Resets the layout and removes any child views.
+ * Called when the view holder is removed.
+ */
+ @CallSuper
+ open fun disconnect() {
+ updateConstraints {
+ connect(R.id.label, START, PARENT_ID, START)
+ connect(R.id.label, END, PARENT_ID, END)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt
new file mode 100644
index 0000000000..9f1612ef9b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter.icons
+
+import android.view.LayoutInflater
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.ext.applyStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+
+internal class TextMenuIconViewHolder(
+ parent: ConstraintLayout,
+ inflater: LayoutInflater,
+ side: Side,
+) : MenuIconViewHolder<TextMenuIcon>(parent, inflater) {
+
+ private val textView: TextView = inflate(layoutResource).findViewById(R.id.icon)
+
+ init {
+ updateConstraints {
+ connect(textView.id, TOP, PARENT_ID, TOP)
+ connect(textView.id, BOTTOM, PARENT_ID, BOTTOM)
+ when (side) {
+ Side.START -> {
+ connect(textView.id, START, PARENT_ID, START)
+ connect(textView.id, END, R.id.label, START)
+ connect(R.id.label, START, textView.id, END)
+ }
+ Side.END -> {
+ connect(textView.id, END, PARENT_ID, END)
+ connect(textView.id, START, R.id.label, END)
+ connect(R.id.label, END, textView.id, START)
+ }
+ }
+ }
+ }
+
+ override fun bind(newIcon: TextMenuIcon, oldIcon: TextMenuIcon?) {
+ textView.text = newIcon.text
+ textView.applyStyle(newIcon.textStyle, oldIcon?.textStyle)
+ }
+
+ override fun disconnect() {
+ parent.removeView(textView)
+ super.disconnect()
+ }
+
+ companion object {
+ @LayoutRes
+ val layoutResource = R.layout.mozac_browser_menu2_icon_text
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt
new file mode 100644
index 0000000000..61fb98aaca
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt
@@ -0,0 +1,261 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.ext
+
+import android.graphics.Rect
+import android.view.View
+import androidx.annotation.Px
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Orientation
+import mozilla.components.support.ktx.android.view.isRTL
+import kotlin.math.roundToInt
+
+const val HALF_MENU_ITEM = 0.5
+
+@Suppress("ComplexMethod")
+internal fun inferMenuPositioningData(
+ containerView: View,
+ anchor: View,
+ style: MenuStyle? = null,
+ orientation: Orientation?,
+): MenuPositioningData? {
+ // Measure the menu allowing it to expand entirely.
+ val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+ containerView.measure(spec, spec)
+
+ val recyclerView = containerView.findViewById<RecyclerView>(R.id.mozac_browser_menu_recyclerView)
+ val recyclerViewAdapter = recyclerView.adapter ?: run {
+ // We might want to track how often and in what circumstances the menu gets called without
+ // valid parameters once we have a system for that.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1814816
+ return null
+ }
+
+ val hasViewInitializedCorrectly = containerView.measuredHeight > 0 && containerView.measuredWidth > 0 &&
+ recyclerView.measuredHeight > 0 && recyclerViewAdapter.itemCount > 0
+ if (!hasViewInitializedCorrectly) {
+ // Same as above: https://bugzilla.mozilla.org/show_bug.cgi?id=1814816
+ return null
+ }
+
+ val menuHorizontalPadding = containerView.measuredWidth - recyclerView.measuredWidth
+ val menuVerticalPadding = containerView.measuredHeight - recyclerView.measuredHeight
+ var horizontalOffset = style?.horizontalOffset ?: 0
+ var verticalOffset = style?.verticalOffset ?: 0
+
+ // Elevation creates some padding between the menu and its container, so that the start corner
+ // of the menu doesn't match the corner of the anchor view. If the user wants the menu to hide
+ // the anchor completely, we have to adjust the position of the menu to compensate for the inner
+ // padding of the menu container.
+ if (style?.completelyOverlap == true) {
+ horizontalOffset -= menuHorizontalPadding / 2
+ verticalOffset -= menuVerticalPadding / 2
+ }
+
+ // The menu height might be adjusted: if there is not enough space to show all the items,
+ // it will crop the last visible item in half, to give the user a hint that it is scrollable.
+ val containerViewHeight = calculateContainerHeight(
+ recyclerView.measuredHeight,
+ recyclerViewAdapter.itemCount,
+ containerView.measuredHeight,
+ style?.verticalOffset ?: 0,
+ anchor,
+ )
+
+ val (availableHeightToTop, availableHeightToBottom) = getMaxAvailableHeightToTopAndBottom(anchor)
+ val (availableWidthToLeft, availableWidthToRight) = getMaxAvailableWidthToLeftAndRight(anchor)
+
+ val fitsUp = availableHeightToTop + anchor.height >= containerViewHeight
+ val fitsDown = availableHeightToBottom + anchor.height >= containerViewHeight
+ val fitsRight = availableWidthToRight + anchor.width >= containerView.measuredWidth
+ val fitsLeft = availableWidthToLeft + anchor.width >= containerView.measuredWidth
+
+ val notEnoughHorizontalSpace = !fitsRight && !fitsLeft
+ val fitsBothHorizontalDirections = fitsRight && fitsLeft
+ val drawingLeft = if (notEnoughHorizontalSpace || fitsBothHorizontalDirections) {
+ anchor.isRTL
+ } else {
+ !fitsRight
+ }
+
+ val anchorPosition = IntArray(2)
+ anchor.getLocationInWindow(anchorPosition)
+ var (anchorX, anchorY) = anchorPosition
+
+ // Position the menu above the anchor if the orientation is UP and there is enough space.
+ if (orientation == Orientation.UP && fitsUp) {
+ anchorY -= containerViewHeight - anchor.height
+ verticalOffset = -verticalOffset
+ }
+
+ if (drawingLeft) {
+ anchorX -= containerView.measuredWidth - anchor.width
+ horizontalOffset = -horizontalOffset
+ }
+
+ return MenuPositioningData(
+ anchor = anchor,
+ x = anchorX + horizontalOffset,
+ y = anchorY + verticalOffset,
+ containerHeight = containerViewHeight,
+ animation = getAnimation(fitsUp, fitsDown, drawingLeft, orientation),
+ )
+}
+
+private fun getMaxAvailableHeightToTopAndBottom(anchor: View): Pair<Int, Int> {
+ val anchorPosition = IntArray(2)
+ val displayFrame = Rect()
+
+ val appView = anchor.rootView
+ appView.getWindowVisibleDisplayFrame(displayFrame)
+
+ anchor.getLocationOnScreen(anchorPosition)
+
+ val bottomEdge = displayFrame.bottom
+
+ val distanceToBottom = bottomEdge - (anchorPosition[1] + anchor.height)
+ val distanceToTop = anchorPosition[1] - displayFrame.top
+
+ return distanceToTop to distanceToBottom
+}
+
+private fun getMaxAvailableWidthToLeftAndRight(anchor: View): Pair<Int, Int> {
+ val anchorPosition = IntArray(2)
+ val displayFrame = Rect()
+
+ val appView = anchor.rootView
+ appView.getWindowVisibleDisplayFrame(displayFrame)
+
+ anchor.getLocationOnScreen(anchorPosition)
+
+ val distanceToLeft = anchorPosition[0] - displayFrame.left
+ val distanceToRight = displayFrame.right - (anchorPosition[0] + anchor.width)
+
+ return distanceToLeft to distanceToRight
+}
+
+/**
+ * Determine whether the container view can display all menu items (without scrolling) within
+ * the available height.
+ *
+ * @return The original container height if the container view can display all menu items
+ * (without scrolling), else calculate the maximum available container height for a scrollable
+ * view with a half menu item.
+ */
+private fun calculateContainerHeight(
+ recyclerViewHeight: Int,
+ recyclerViewItemCount: Int,
+ containerViewHeight: Int,
+ menuStylePadding: Int,
+ anchor: View,
+): Int {
+ // Get the total screen display height.
+ val totalHeight = anchor.rootView.measuredHeight
+
+ // Note: We cannot use getWindowVisibleDisplayFrame() as the height is dynamic based on whether
+ // the keyboard is open.
+ // Get any displayed system bars e.g. top status bar, bottom navigation bar or soft buttons bar.
+ val systemBars =
+ ViewCompat.getRootWindowInsets(anchor)?.getInsets(WindowInsetsCompat.Type.systemBars())
+ // Store the vertical status bars.
+ val topSystemBarHeight = systemBars?.top ?: 0
+ val bottomSystemBarHeight = systemBars?.bottom ?: 0
+
+ // Deduct any status bar heights from the total height.
+ val availableHeight = totalHeight - (bottomSystemBarHeight + topSystemBarHeight)
+
+ val menuItemHeight = recyclerViewHeight / recyclerViewItemCount
+
+ // We must take the menu container padding into account as this will be applied to the final height.
+ val containerPadding = containerViewHeight - recyclerViewHeight
+
+ val maxAvailableHeightForRecyclerView = availableHeight - containerPadding - menuStylePadding
+
+ // The number of menu items that can fit exactly (no cropping) within the max app height.
+ // Round the number of items to the closet Int value to ensure the max space available is utilized.
+ // E.g if 6.9 items fit, round to 7 so the calculation below will show 6.5 items instead of 5.5 .
+ val numberOfItemsFitExactly =
+ (maxAvailableHeightForRecyclerView.toFloat() / menuItemHeight.toFloat()).roundToInt()
+
+ val itemsAlreadyFitContainerHeight = recyclerViewItemCount <= numberOfItemsFitExactly
+
+ return if (itemsAlreadyFitContainerHeight) {
+ containerViewHeight
+ } else {
+ getCroppedMenuContainerHeight(numberOfItemsFitExactly, menuItemHeight, containerPadding)
+ }
+}
+
+private fun getAnimation(
+ fitsUp: Boolean,
+ fitsDown: Boolean,
+ drawingLeft: Boolean,
+ orientation: Orientation?,
+): Int {
+ val isUpOrientation = orientation == Orientation.UP
+ return when {
+ isUpOrientation && fitsUp -> if (drawingLeft) {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom
+ } else {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom
+ }
+ fitsDown -> if (drawingLeft) {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop
+ } else {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop
+ }
+ else -> if (drawingLeft) {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight
+ } else {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft
+ }
+ }
+}
+
+private fun getCroppedMenuContainerHeight(
+ numberOfItemsFitExactly: Int,
+ menuItemHeight: Int,
+ containerPadding: Int,
+): Int {
+ // The number of menu items that fit exactly, minus a half menu item (indicates more menu items exist).
+ val numberOfItemsFitWithOverflow = numberOfItemsFitExactly - HALF_MENU_ITEM
+ val updatedRecyclerViewHeight = (numberOfItemsFitWithOverflow * menuItemHeight).toInt()
+
+ return updatedRecyclerViewHeight + containerPadding
+}
+
+/**
+ * Data needed for menu positioning.
+ */
+data class MenuPositioningData(
+ /**
+ * Android View that the PopupWindow should be anchored to.
+ */
+ val anchor: View,
+
+ /**
+ * [WindowManager#LayoutParams#x] of params the menu will be added with.
+ */
+ @Px val x: Int = 0,
+
+ /**
+ * [WindowManager#LayoutParams#y] of params the menu will be added with.
+ */
+ @Px val y: Int = 0,
+
+ /**
+ * [View#measuredHeight] of the menu.
+ */
+ @Px val containerHeight: Int = 0,
+
+ /**
+ * [PopupWindow#animationStyle] of the menu.
+ */
+ val animation: Int,
+)
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt
new file mode 100644
index 0000000000..b40e893955
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.ext
+
+import android.content.res.ColorStateList
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuCandidateEffect
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.concept.menu.candidate.MenuIconWithDrawable
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+
+/**
+ * Apply container styles if different from the previous styling.
+ */
+internal fun View.applyStyle(newStyle: ContainerStyle, oldStyle: ContainerStyle?) {
+ if (newStyle != oldStyle) {
+ isVisible = newStyle.isVisible
+ isEnabled = newStyle.isEnabled
+ }
+}
+
+/**
+ * Apply text styles if different from the previous styling.
+ */
+internal fun TextView.applyStyle(newStyle: TextStyle, oldStyle: TextStyle?) {
+ if (newStyle != oldStyle) {
+ newStyle.size?.let { textSize = it }
+ newStyle.color?.let { setTextColor(it) }
+ setTypeface(typeface, newStyle.textStyle)
+ textAlignment = newStyle.textAlignment
+ }
+}
+
+/**
+ * Set the image to display based on the [MenuIconWithDrawable].
+ */
+internal fun ImageView.applyIcon(newIcon: MenuIconWithDrawable, oldIcon: MenuIconWithDrawable?) {
+ if (newIcon != oldIcon) {
+ setImageDrawable(newIcon.drawable)
+ imageTintList = newIcon.tint?.let { ColorStateList.valueOf(it) }
+ }
+}
+
+internal fun ImageView.applyNotificationEffect(
+ newEffect: LowPriorityHighlightEffect?,
+ oldEffect: MenuEffect?,
+) {
+ if (newEffect != oldEffect) {
+ isVisible = newEffect != null
+ imageTintList = newEffect?.notificationTint?.let { ColorStateList.valueOf(it) }
+ }
+}
+
+/**
+ * Build a drawable to be used for the background of a menu option.
+ */
+internal fun View.applyBackgroundEffect(
+ newEffect: MenuCandidateEffect?,
+ oldEffect: MenuCandidateEffect?,
+) {
+ if (newEffect == oldEffect) return
+
+ val highlight = newEffect as? HighPriorityHighlightEffect
+ val selectableBackgroundRes = context.theme
+ .resolveAttribute(android.R.attr.selectableItemBackground)
+
+ if (highlight != null) {
+ val selectableBackground = ContextCompat.getDrawable(
+ context,
+ selectableBackgroundRes,
+ )
+
+ setBackgroundColor(highlight.backgroundTint)
+ if (SDK_INT >= Build.VERSION_CODES.M) {
+ foreground = selectableBackground
+ }
+ } else {
+ setBackgroundResource(selectableBackgroundRes)
+ if (SDK_INT >= Build.VERSION_CODES.M) {
+ foreground = null
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt
new file mode 100644
index 0000000000..eeba85dede
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.view
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.MenuButton
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.MenuEffect
+import mozilla.components.concept.menu.ext.effects
+import mozilla.components.concept.menu.ext.max
+import mozilla.components.support.base.observer.Observable
+import mozilla.components.support.base.observer.ObserverRegistry
+import mozilla.components.support.ktx.android.view.hideKeyboard
+
+/**
+ * A `three-dot` button used for expanding menus.
+ *
+ * If you are using a browser toolbar, do not use this class directly.
+ */
+class MenuButton2 @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr),
+ MenuButton,
+ View.OnClickListener,
+ Observable<MenuButton.Observer> by ObserverRegistry() {
+
+ /**
+ * Sets a [MenuController] that will be used to create a menu when this button is clicked.
+ */
+ override var menuController: MenuController? = null
+ set(value) {
+ // Clean up old controller
+ field?.dismiss()
+ field?.unregister(menuControllerObserver)
+
+ // Attach new controller
+ field = value
+ value?.register(menuControllerObserver, this)
+ }
+
+ private val menuControllerObserver = object : MenuController.Observer {
+ /**
+ * Change the menu button appearance when the menu list changes.
+ */
+ override fun onMenuListSubmit(list: List<MenuCandidate>) {
+ val effect = list.effects().max()
+
+ // If a highlighted item is found, show the indicator
+ setEffect(effect)
+ }
+
+ override fun onDismiss() = notifyObservers { onDismiss() }
+ }
+
+ private val menuIcon: ImageView
+ private val highlightView: ImageView
+ private val notificationIconView: ImageView
+
+ init {
+ View.inflate(context, R.layout.mozac_browser_menu2_button, this)
+ setOnClickListener(this)
+ menuIcon = findViewById(R.id.icon)
+ highlightView = findViewById(R.id.highlight)
+ notificationIconView = findViewById(R.id.notification_dot)
+ }
+
+ /**
+ * Shows the menu.
+ */
+ override fun onClick(v: View) {
+ this.hideKeyboard()
+ val menuController = menuController ?: return
+
+ menuController.show(anchor = this)
+ notifyObservers { onShow() }
+ }
+
+ /**
+ * Show the indicator for a browser menu effect.
+ */
+ override fun setEffect(effect: MenuEffect?) {
+ when (effect) {
+ is HighPriorityHighlightEffect -> {
+ highlightView.imageTintList = ColorStateList.valueOf(effect.backgroundTint)
+ highlightView.visibility = View.VISIBLE
+ notificationIconView.visibility = View.GONE
+ }
+ is LowPriorityHighlightEffect -> {
+ notificationIconView.setColorFilter(effect.notificationTint)
+ highlightView.visibility = View.GONE
+ notificationIconView.visibility = View.VISIBLE
+ }
+ null -> {
+ highlightView.visibility = View.GONE
+ notificationIconView.visibility = View.GONE
+ }
+ }
+ }
+
+ /**
+ * Sets the tint of the 3-dot menu icon.
+ */
+ override fun setColorFilter(@ColorInt color: Int) = menuIcon.setColorFilter(color)
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt
new file mode 100644
index 0000000000..d0d0e5d96b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.view
+
+import android.content.Context
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+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
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.MenuCandidateListAdapter
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.support.ktx.android.view.onNextGlobalLayout
+
+/**
+ * A popup menu composed of [MenuCandidate] objects.
+ */
+class MenuView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ private val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ private val menuAdapter = MenuCandidateListAdapter(
+ inflater = LayoutInflater.from(context),
+ dismiss = { onDismiss() },
+ reopenMenu = { onReopenMenu(it) },
+ )
+ private val cardView: CardView
+ private val recyclerView: RecyclerView
+
+ /**
+ * Called when the menu is clicked and should be dismissed.
+ */
+ var onDismiss: () -> Unit = {}
+
+ /**
+ * Called when a nested menu should be opened.
+ */
+ var onReopenMenu: (NestedMenuCandidate?) -> Unit = {}
+
+ init {
+ View.inflate(context, R.layout.mozac_browser_menu2_view, this)
+
+ cardView = findViewById(R.id.mozac_browser_menu_cardView)
+ recyclerView = findViewById(R.id.mozac_browser_menu_recyclerView)
+ recyclerView.layoutManager = layoutManager
+ recyclerView.adapter = menuAdapter
+ }
+
+ /**
+ * Changes the contents of the menu.
+ */
+ fun submitList(list: List<MenuCandidate>?) = menuAdapter.submitList(list)
+
+ /**
+ * Displays either the start or the end of the list.
+ */
+ fun setVisibleSide(side: Side) {
+ if (SDK_INT >= Build.VERSION_CODES.N) {
+ layoutManager.stackFromEnd = side == Side.END
+ } else {
+ // 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)
+ }
+ }
+
+ /**
+ * Sets the background color for the menu view.
+ */
+ fun setStyle(style: MenuStyle) {
+ style.backgroundColor?.let { cardView.setCardBackgroundColor(it) }
+ }
+
+ @VisibleForTesting
+ internal fun scrollOnceToTheBottom(recyclerView: RecyclerView) {
+ recyclerView.onNextGlobalLayout {
+ recyclerView.adapter?.let { recyclerView.scrollToPosition(it.itemCount - 1) }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.xml
new file mode 100644
index 0000000000..de510ecb12
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="5%"
+ android:pivotY="50%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.xml
new file mode 100644
index 0000000000..6d27c410ea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="5%"
+ android:pivotY="100%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.xml
new file mode 100644
index 0000000000..fc141bdbd0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="5%"
+ android:pivotY="5%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.xml
new file mode 100644
index 0000000000..0ca98399a7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="95%"
+ android:pivotY="50%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.xml
new file mode 100644
index 0000000000..89153ca6e5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="95%"
+ android:pivotY="100%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.xml
new file mode 100644
index 0000000000..f0485403ef
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="0"
+ android:toXScale="1"
+ android:fromYScale="0"
+ android:toYScale="1"
+ android:pivotX="95%"
+ android:pivotY="5%"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="@android:integer/config_shortAnimTime" />
+ <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml
new file mode 100644
index 0000000000..226166c109
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha android:interpolator="@android:anim/linear_interpolator"
+ android:fromAlpha="1"
+ android:toAlpha="0"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml
new file mode 100644
index 0000000000..33d8ad19b4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml
@@ -0,0 +1,29 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <group
+ android:scaleX="0.714"
+ android:scaleY="0.714"
+ android:pivotX="20"
+ android:pivotY="20">
+ <path
+ android:pathData="m38.622,27.309a8,8 0,0 0,-11.314 11.314,19.949 19.949,0 0,1 -7.308,1.377c-11.046,0 -20,-8.954 -20,-20s8.954,-20 20,-20 20,8.954 20,20c0,2.58 -0.488,5.045 -1.378,7.309z"
+ android:fillColor="#ffffff"
+ android:fillAlpha=".4" />
+ <path
+ android:pathData="M33,33m-6.4,0a6.4,6.4 0,1 1,12.8 0a6.4,6.4 0,1 1,-12.8 0"
+ android:fillColor="#ffffff"
+ android:fillAlpha=".4" />
+ <path
+ android:pathData="M33,33m-4.3,0a4.3,4.3 0,1 1,8.6 0a4.3,4.3 0,1 1,-8.6 0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#ffffff"
+ android:strokeColor="#000000" />
+ </group>
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml
new file mode 100644
index 0000000000..b7b5ced0c0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml
@@ -0,0 +1,21 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <group
+ android:translateX="25.55"
+ android:translateY="5.55"
+ android:scaleX="0.75"
+ android:scaleY="0.75">
+ <path
+ android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#fff"
+ android:strokeColor="#000" />
+ </group>
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml
new file mode 100644
index 0000000000..8afb175afe
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml
@@ -0,0 +1,15 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="8dp"
+ android:height="8dp"
+ android:viewportWidth="10"
+ android:viewportHeight="10">
+ <path
+ android:pathData="M1,5a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"
+ android:strokeWidth="1"
+ android:strokeAlpha=".2"
+ android:fillColor="#fff"
+ android:strokeColor="#000" />
+</vector>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml
new file mode 100644
index 0000000000..0d31537664
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="?android:selectableItemBackgroundBorderless"
+ tools:parentTag="android.widget.FrameLayout">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/highlight"
+ app:srcCompat="@drawable/mozac_browser_menu2_indicator"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu2_highlighted"
+ android:visibility="gone" />
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/icon"
+ app:srcCompat="@drawable/mozac_ic_ellipsis_vertical_24"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu2_button" />
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/notification_dot"
+ app:srcCompat="@drawable/mozac_browser_menu2_notification"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="center"
+ android:contentDescription="@string/mozac_browser_menu2_highlighted"
+ android:visibility="gone" />
+
+</merge>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml
new file mode 100644
index 0000000000..eba14d8e6d
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu2.Candidate.Container"
+ android:layout_width="match_parent"
+ android:paddingStart="@dimen/mozac_browser_menu2_candidate_container_padding_start"
+ android:paddingEnd="0dp"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ tools:ignore="DisableBaselineAlignment,KeyboardInaccessibleWidget">
+
+ <androidx.appcompat.widget.AppCompatCheckBox
+ android:id="@+id/label"
+ style="@style/Mozac.Browser.Menu2.Candidate.Label"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/mozac_browser_menu2_candidate_container_layout_height"
+ android:background="@null"
+ android:button="@null"
+ android:drawableEnd="?android:attr/listChoiceIndicatorMultiple"
+ android:drawablePadding="@dimen/mozac_browser_menu2_checkbox_padding"
+ android:paddingStart="0dp"
+ android:paddingEnd="@dimen/mozac_browser_menu2_candidate_container_padding_end"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:gravity="center_vertical"
+ tools:text="Item" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml
new file mode 100644
index 0000000000..d702796ce8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu2.Candidate.Container"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ tools:ignore="DisableBaselineAlignment,KeyboardInaccessibleWidget">
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/label"
+ style="@style/Mozac.Browser.Menu2.Candidate.Label"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/mozac_browser_menu2_candidate_container_layout_height"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:gravity="center_vertical"
+ tools:text="Item" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml
new file mode 100644
index 0000000000..f2fc0aa9d3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/simple_text"
+ style="@style/Mozac.Browser.Menu2.Candidate.Text"
+ android:background="@null"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu2_candidate_container_layout_height"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="start|center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:textAlignment="viewStart"
+ tools:text="Item" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml
new file mode 100644
index 0000000000..b716250ca9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Mozac.Browser.Menu2.Candidate.Divider.Horizontal"
+ android:importantForAccessibility="no"
+ android:clickable="false"/>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml
new file mode 100644
index 0000000000..4eff0a72a4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu2.Candidate.Container"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center_vertical">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/label"
+ style="@style/Mozac.Browser.Menu2.Candidate.Label"
+ android:layout_width="0dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ android:background="@null"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ tools:text="Item" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml
new file mode 100644
index 0000000000..a46826e4b4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/TextAppearance.Material.Menu"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/mozac_browser_menu2_candidate_row_height"
+ android:gravity="center_vertical"
+ android:orientation="horizontal" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml
new file mode 100644
index 0000000000..02f5bb4ebc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.appcompat.widget.AppCompatImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ tools:src="@android:color/background_dark" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml
new file mode 100644
index 0000000000..4eff0a72a4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="@style/Mozac.Browser.Menu2.Candidate.Container"
+ android:layout_width="match_parent"
+ android:orientation="horizontal"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center_vertical">
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/label"
+ style="@style/Mozac.Browser.Menu2.Candidate.Label"
+ android:layout_width="0dp"
+ android:clickable="false"
+ android:focusable="false"
+ android:gravity="center_vertical"
+ android:background="@null"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ tools:text="Item" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml
new file mode 100644
index 0000000000..bb60166414
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.appcompat.widget.AppCompatImageButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/icon"
+ style="@style/Mozac.Browser.Menu2.Icon"
+ tools:src="@android:drawable/ic_menu_add" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml
new file mode 100644
index 0000000000..b10ae873e0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.appcompat.widget.AppCompatImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/icon"
+ style="@style/Mozac.Browser.Menu2.Icon"
+ android:clickable="false"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ tools:src="@android:drawable/ic_menu_add" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml
new file mode 100644
index 0000000000..54c6c509d6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.appcompat.widget.AppCompatImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/notification_dot"
+ android:layout_gravity="top|end"
+ android:contentDescription="@string/mozac_browser_menu2_highlighted"
+ android:layout_width="@dimen/mozac_browser_menu2_icon_notification_dot_size"
+ android:layout_height="@dimen/mozac_browser_menu2_icon_notification_dot_size"
+ android:translationX="@dimen/mozac_browser_menu2_icon_notification_dot_translate_x"
+ android:translationY="@dimen/mozac_browser_menu2_icon_notification_dot_translate_y"
+ android:background="@android:color/transparent"
+ app:srcCompat="@drawable/mozac_browser_menu2_notification_icon" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml
new file mode 100644
index 0000000000..9e94057898
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<androidx.appcompat.widget.AppCompatTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/icon"
+ style="@style/Mozac.Browser.Menu2.Icon.Text"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAlignment="viewEnd"
+ tools:text="Ctrl+X" />
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml
new file mode 100644
index 0000000000..1e05515a6f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:parentTag="FrameLayout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <androidx.cardview.widget.CardView
+ style="@style/Mozac.Browser.Menu2"
+ android:id="@+id/mozac_browser_menu_cardView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:cardCornerRadius="@dimen/mozac_browser_menu2_corner_radius"
+ app:cardElevation="@dimen/mozac_browser_menu2_elevation"
+ app:cardUseCompatPadding="true">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/mozac_browser_menu_recyclerView"
+ android:paddingTop="@dimen/mozac_browser_menu2_padding_vertical"
+ android:paddingBottom="@dimen/mozac_browser_menu2_padding_vertical"
+ android:layout_width="@dimen/mozac_browser_menu2_width"
+ android:layout_height="wrap_content"
+ android:overScrollMode="never"
+ tools:listitem="@layout/mozac_browser_menu2_candidate_text" />
+
+ </androidx.cardview.widget.CardView>
+</merge>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..79d77db735
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ምናሌ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">የተተኮረበት</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..3fc9124078
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacaus</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..41f0bf6f11
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">القائمة</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">عليها الإبراز</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..08ab9cea55
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Rescamplóse</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..d3163532fb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Vurğulanmış</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..6e0898fb50
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">منو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">هایلایت اولدو</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..5a0a67e133
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Kasorot</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..c335f20e0b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Вылучаны(я)</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..6a42322297
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Откроено</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..35491cbc2b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">মেনু</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">হাইলাইট করা হয়েছে</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..3377f9e8ec
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Lañser</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Usskedet</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..e4be501bc9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Istaknuto</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..f34fc74e2e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">S’ha ressaltat</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..32d2e53097
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">K\'utsamaj</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Ya\'on ruq\'ij</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..c31b0a90da
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Gipasiugda</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..d935386713
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">پێڕست</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ئاماژەپێکراو</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..afa726fc94
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Listinu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Sopralineatu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..b027ce6479
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Nabídka</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Zvýrazněné</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..fa069ca454
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Dewislen</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Amlygwyd</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..dca8b4f467
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Fremhævet</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..be63b6937a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Hervorgehoben</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..97cc5b8efc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Wuzwignjony</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..c6dfb2b15a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Μενού</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Επισημασμένο</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..592d520cac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Highlighted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..592d520cac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Highlighted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..36f886c443
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menuo</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Elstarigitaj</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..85c3558fdf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Resaltado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..ff96f0b1a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..85c3558fdf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Resaltado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..ff96f0b1a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..85c3558fdf
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Resaltado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..48d020d392
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menüü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Esiletõstetud</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..fe9175c4cd
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menua</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Nabarmendua</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..c725cf390f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">منو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">برجسته‌شده</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..2df10018d3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Valikko</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Korostettu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..0c17d18f2f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Sélectionné</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..295ae072f1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidenziât</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..df69165f85
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Markearre</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..085e65afa1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">An clàr-taice</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Soillsichte</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..3cc8bc80c7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Realzado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..99dd9a29bc
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Poravorã</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Hechaukaveha</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..a3e12d11ea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">મેનુ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">પ્રકાશિત કરેલ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..c26c684891
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">मेन्यू</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">दर्शाए गए</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..592d520cac
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Highlighted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..6b6f279bf5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Izbornik</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Istaknuto</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..a646978d65
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Wuzběhnjeny</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..8b3a5be2d7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Kiemelt</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..95863f1c45
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Ցանկ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Գունանշված</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..273756232a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidentiate</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..bdbc257bca
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Tersorot</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..25470860c2
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Valmynd</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Undirstrikað</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..bcda59c692
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidenziato</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..1e259b99f4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">תפריט</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">מודגש</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..573ca56cea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">メニュー</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">強調</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..bcda2c40f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">მენიუ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">მონიშნული</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..5933838259
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Belgilengen</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..01b0200a52
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Umuɣ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ittwag deg uqerru</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..dc3e56e0a3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Мәзір</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Ерекшеленген</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..fef72a1d0e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menû</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Berbiçavkirî</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..eedd3a4d96
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ಮೆನು</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ಹೈಲೈಟ್ ಮಾಡಲಾಗಿದೆ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..6106e64081
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">메뉴</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">강조 표시됨</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml
new file mode 100644
index 0000000000..6ed87c5d94
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Rol</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Golowboyntys</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml
new file mode 100644
index 0000000000..5dea33699b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <dimen name="mozac_browser_menu2_icon_notification_dot_translate_x">-4dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..3e379e7a7e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">In evidensa</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..0feac7d979
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ເມນູ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ຈຸດເດັ່ນ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..fef1bedd5b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meniu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Paryškinta</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..fff2691aea
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Katsi</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Tu^un nchichi</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..c03e62b772
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">मेनू</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ठळक</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..1ddc43e6f8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">မီနူး</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">အသားပေးအရာ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..33264b5c05
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Uthevet</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..3040e760b8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">मेनु</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">हाइलाइट गरियो</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..0b7f6dfe19
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Gemarkeerd</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..1ae9d7d81f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Utheva</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..9ead4cdd58
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menú</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Notables</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000000..dcf46d2ff3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ମେନୁ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ହାଇଲାଇଟ୍ କରାଯାଇଥିବା</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..e9704bf09f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ਮੀਨੂ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ਉਘਾੜੇ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..0034100a2a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">مینو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">اُگھاڑے</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..d430f3d2b3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Wyróżnione</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..17464e70f5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..17464e70f5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Destacado</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..3ff1b693a9
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Cun emfasa</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..36c3bbc33e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meniu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidențiat</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..edaedb47a7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Выделено</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..d0d78ee8df
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">ᱢᱮᱱᱩ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ᱩᱪᱷᱟᱹᱱᱟᱜ</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..47781dced8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">In evidèntzia</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..360b4ba100
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">වට්ටෝරුව</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ත්‍රීවාලෝකිත</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..83ef834d61
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Ponuka</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Zvýraznené</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..92d8c77be3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">مینیو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">نمایاں کیتا ڳیا</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..5f76d3d0a4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Označeno</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..5059b3ddc0
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">E theksuar</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..771684ae37
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Мени</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Истакнуто</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..f5694fa656
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Disorot</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..53e1a2c120
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Meny</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Markerad</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..050fdcd8f7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Myni</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Ôbznoczōne</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..ef67a234f3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">பட்டி</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">மிளிர்ப்புகள்</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..6a8494e72c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">మెనూ</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">హైలైట్ చేసినవి</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..f9874801ec
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Таъкид</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..3b39bcac10
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">เมนู</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">เน้น</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..83bba47bf4
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Mga naka-highlight</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..4faac7b60a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menü</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Vurgulu</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..da59e49395
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menû</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Sa ña\'āan</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..33e7485245
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Аерылган</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..671152685f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Umuɣ</string>
+ </resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..444cf11219
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">تىزىملىك</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">ئالاھىدە</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..f5ecb3600e
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Меню</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Виділено</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..b19fad90ee
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">مینیو</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">نمایاں کیا گیا</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..45ec9ab80f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menyu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Belgilangan</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..e0e190062a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Evidensià</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..f3544ed037
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Đã tô sáng</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..14377ebe7c
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">mẹ́nù</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Ti fàmìsí</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..fedb88a7f7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">菜单</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">高亮</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..d7ce12c735
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">選單</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">強調</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..a77289bfe3
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <!-- Empty by default, allows others to theme as they see fit -->
+ <color name="mozac_browser_menu2_background"></color>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..fa390df86f
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <dimen name="mozac_browser_menu2_corner_radius">4dp</dimen>
+ <dimen name="mozac_browser_menu2_elevation">6dp</dimen>
+ <dimen name="mozac_browser_menu2_width">250dp</dimen>
+ <dimen name="mozac_browser_menu2_padding_vertical">0dp</dimen>
+
+ <!--Menu Item -->
+ <dimen name="mozac_browser_menu2_candidate_text_size">16sp</dimen>
+ <dimen name="mozac_browser_menu2_candidate_container_layout_height">48dp</dimen>
+ <dimen name="mozac_browser_menu2_candidate_container_padding_start">16dp</dimen>
+ <dimen name="mozac_browser_menu2_candidate_container_padding_end">16dp</dimen>
+ <!--Menu Item -->
+
+ <!--BrowserMenuDivider -->
+ <dimen name="mozac_browser_menu2_candidate_divider_height">1dp</dimen>
+ <!--BrowserMenuDivider -->
+
+ <!--BrowserMenuHighlightableItem -->
+ <dimen name="mozac_browser_menu2_icon_notification_dot_translate_x">4dp</dimen>
+ <dimen name="mozac_browser_menu2_icon_notification_dot_translate_y">-4dp</dimen>
+ <dimen name="mozac_browser_menu2_icon_notification_dot_size">8dp</dimen>
+ <!--BrowserMenuHighlightableItem -->
+
+ <!--BrowserMenuCheckbox -->
+ <dimen name="mozac_browser_menu2_checkbox_padding">12dp</dimen>
+ <!--BrowserMenuCheckbox -->
+
+ <!--BrowserMenuImageText-->
+
+ <!--Icon-->
+ <dimen name="mozac_browser_menu2_icon_width">24dp</dimen>
+ <dimen name="mozac_browser_menu2_icon_height">24dp</dimen>
+ <dimen name="mozac_browser_menu2_icon_text_size">14sp</dimen>
+ <!--Icon-->
+
+ <!--Label-->
+ <dimen name="mozac_browser_menu2_icon_padding_start">20dp</dimen>
+ <!--Label-->
+
+ <!--BrowserMenuImageText-->
+
+ <!-- BrowserMenuItemToolbar -->
+ <dimen name="mozac_browser_menu2_candidate_row_height">56dp</dimen>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..ebcceba5ad
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
+ <string name="mozac_browser_menu2_button">Menu</string>
+ <!-- Content description (not visible, for screen readers etc.): Indicates the overflow menu has a highlight -->
+ <string name="mozac_browser_menu2_highlighted">Highlighted</string>
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml b/mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml
new file mode 100644
index 0000000000..eb2702e6f1
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<resources>
+
+ <style name="Mozac.Browser.Menu2" parent="">
+ <item name="cardBackgroundColor">@color/mozac_browser_menu2_background</item>
+ </style>
+
+ <!-- Item Divider -->
+ <style name="Mozac.Browser.Menu2.Candidate.Divider" parent="">
+ <item name="android:background">?android:attr/listDivider</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Candidate.Divider.Horizontal">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/mozac_browser_menu2_candidate_divider_height</item>
+ </style>
+ <!-- Item Divider -->
+
+ <style name="Mozac.Browser.Menu2.Candidate.Container" parent="">
+ <item name="android:layout_height">@dimen/mozac_browser_menu2_candidate_container_layout_height</item>
+ <item name="android:paddingStart">@dimen/mozac_browser_menu2_candidate_container_padding_start</item>
+ <item name="android:paddingEnd">@dimen/mozac_browser_menu2_candidate_container_padding_end</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Candidate.Text" parent="@android:style/TextAppearance.Material.Menu">
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:textSize">@dimen/mozac_browser_menu2_candidate_text_size</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:lines">1</item>
+ <item name="android:focusable">true</item>
+ <item name="android:clickable">true</item>
+ </style>
+
+ <!-- BrowserMenuImageText -->
+ <style name="Mozac.Browser.Menu2.Icon" parent="">
+ <item name="android:layout_width">@dimen/mozac_browser_menu2_icon_width</item>
+ <item name="android:layout_height">@dimen/mozac_browser_menu2_icon_height</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Icon.Text" parent="Mozac.Browser.Menu2.Icon">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:textSize">@dimen/mozac_browser_menu2_icon_text_size</item>
+ <item name="android:lines">1</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Candidate.Label" parent="Mozac.Browser.Menu2.Candidate.Text">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+ <!-- BrowserMenuImageText -->
+
+ <!-- Animation -->
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuLeftTop" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_left_top</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuRightTop" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_right_top</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuLeftBottom" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_left_bottom</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuRightBottom" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_right_bottom</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuLeft" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_left</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+
+ <style name="Mozac.Browser.Menu2.Animation.OverflowMenuRight" parent="">
+ <item name="android:windowEnterAnimation">@anim/menu_enter_right</item>
+ <item name="android:windowExitAnimation">@anim/menu_exit</item>
+ </style>
+ <!-- Animation -->
+</resources>
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt
new file mode 100644
index 0000000000..688b77b578
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2
+
+import android.view.Gravity
+import android.widget.Button
+import android.widget.PopupWindow
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu2.ext.MenuPositioningData
+import mozilla.components.browser.menu2.ext.createAnchor
+import mozilla.components.browser.menu2.ext.createContainerView
+import mozilla.components.browser.menu2.ext.getTargetCoordinates
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.MenuCandidate
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuControllerTest {
+
+ @Test
+ fun `created popup window is displayed automatically`() {
+ val menu: MenuController = BrowserMenuController()
+ menu.submitList(listOf(DecorativeTextMenuCandidate("Hello")))
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertTrue(popup.isShowing)
+ }
+
+ @Test
+ fun `WHEN an empty list is submitted to the menu THEN the menu isn't shown`() {
+ val menu: MenuController = BrowserMenuController()
+ menu.submitList(emptyList())
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertFalse(popup.isShowing)
+ }
+
+ @Test
+ fun `observer is notified when submitList is called`() {
+ var submitted: List<MenuCandidate>? = null
+ val menu: MenuController = BrowserMenuController()
+ menu.register(
+ object : MenuController.Observer {
+ override fun onMenuListSubmit(list: List<MenuCandidate>) {
+ submitted = list
+ }
+
+ override fun onDismiss() = Unit
+ },
+ )
+
+ assertNull(submitted)
+
+ menu.submitList(listOf(DecorativeTextMenuCandidate("Hello")))
+ assertEquals(listOf(DecorativeTextMenuCandidate("Hello")), submitted)
+ }
+
+ @Test
+ fun `dismissing the browser menu will dismiss the popup`() {
+ var dismissed = false
+ val menu: MenuController = BrowserMenuController()
+ menu.submitList(listOf(DecorativeTextMenuCandidate("Hello")))
+
+ menu.register(
+ object : MenuController.Observer {
+ override fun onDismiss() {
+ dismissed = true
+ }
+
+ override fun onMenuListSubmit(list: List<MenuCandidate>) = Unit
+ },
+ )
+
+ menu.dismiss()
+ assertFalse(dismissed)
+
+ val anchor = Button(testContext)
+ val popup = menu.show(anchor)
+
+ assertTrue(popup.isShowing)
+ assertFalse(dismissed)
+
+ menu.dismiss()
+
+ assertFalse(popup.isShowing)
+ assertTrue(dismissed)
+ }
+
+ @Test
+ fun `WHEN displayPopup is called with provided positioning data THEN showAtLocation is called with provided positioning values & menu height and animation are set`() {
+ val containerView = createContainerView()
+ val popupWindow = Mockito.spy(PopupWindow())
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor)
+ val positioningData = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ popupWindow.displayPopup(positioningData)
+
+ assertEquals(containerView.measuredHeight, popupWindow.height)
+ assertEquals(positioningData.animation, popupWindow.animationStyle)
+
+ Mockito.verify(popupWindow).showAtLocation(
+ positioningData.anchor,
+ Gravity.NO_GRAVITY,
+ positioningData.x,
+ positioningData.y,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..13128cbad6
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.Typeface
+import android.view.View
+import android.widget.CompoundButton
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class CompoundMenuCandidateViewHolderTest {
+
+ private val baseCandidate = CompoundMenuCandidate(
+ "hello",
+ isChecked = false,
+ end = CompoundMenuCandidate.ButtonType.CHECKBOX,
+ )
+ private lateinit var view: ConstraintLayout
+ private lateinit var compoundButton: CompoundButton
+
+ @Before
+ fun setup() {
+ val context: Context = mock()
+ view = mock()
+ compoundButton = mock()
+
+ doReturn(context).`when`(view).context
+ doReturn(compoundButton).`when`(view).findViewById<TextView>(R.id.label)
+
+ val resources: Resources = mock()
+ doReturn(resources).`when`(context).resources
+
+ doReturn(mock<Resources.Theme>()).`when`(context).theme
+ }
+
+ @Test
+ fun `sets container state and text on view`() {
+ val holder = CompoundCheckboxMenuCandidateViewHolder(view, mock(), mock())
+
+ holder.bind(baseCandidate)
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ verify(compoundButton).text = "hello"
+ verify(compoundButton).isChecked = false
+ verify(compoundButton).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(compoundButton).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+ }
+
+ @Test
+ fun `sets highlight effect`() {
+ val holder = CompoundSwitchMenuCandidateViewHolder(view, mock(), mock())
+
+ holder.bind(baseCandidate)
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view, never()).setBackgroundResource(anyInt())
+
+ holder.bind(baseCandidate.copy(effect = HighPriorityHighlightEffect(Color.RED)))
+ verify(view).setBackgroundColor(Color.RED)
+ verify(view, never()).setBackgroundResource(anyInt())
+
+ clearInvocations(view)
+
+ holder.bind(baseCandidate.copy(effect = null))
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view).setBackgroundResource(anyInt())
+ }
+
+ @Test
+ fun `sets change listener`() {
+ var dismissed = false
+ val holder = CompoundSwitchMenuCandidateViewHolder(view, mock()) { dismissed = true }
+
+ val candidate = baseCandidate.copy(onCheckedChange = mock())
+ holder.bind(candidate)
+ holder.onCheckedChanged(compoundButton, false)
+
+ assertTrue(dismissed)
+ verify(candidate.onCheckedChange).invoke(false)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..a3fde39c97
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.content.res.Resources
+import android.graphics.Typeface
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class DecorativeTextMenuCandidateViewHolderTest {
+
+ @Test
+ fun `sets container state and text on view`() {
+ val view: TextView = mock()
+ val holder = DecorativeTextMenuCandidateViewHolder(view, mock())
+
+ holder.bind(DecorativeTextMenuCandidate("hello"))
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ verify(view).text = "hello"
+ verify(view).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(view).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+ verify(view, never()).layoutParams = any()
+ }
+
+ @Test
+ fun `sets view height`() {
+ val view: TextView = mock()
+ val params = ViewGroup.LayoutParams(0, 0)
+ val resources: Resources = mock()
+ doReturn(params).`when`(view).layoutParams
+ doReturn(resources).`when`(view).resources
+ doReturn(48).`when`(resources).getDimensionPixelSize(anyInt())
+
+ val holder = DecorativeTextMenuCandidateViewHolder(view, mock())
+
+ holder.bind(DecorativeTextMenuCandidate("hello", height = 30))
+ assertEquals(30, params.height)
+ verify(view).layoutParams = params
+
+ clearInvocations(view)
+
+ holder.bind(DecorativeTextMenuCandidate("hello"))
+ assertEquals(48, params.height)
+ verify(view).layoutParams = params
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..493c1d517a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.View
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class DividerMenuCandidateViewHolderTest {
+
+ @Test
+ fun `sets visible and enabled state on divider view`() {
+ val view: View = mock()
+ val holder = DividerMenuCandidateViewHolder(view, mock())
+
+ holder.bind(DividerMenuCandidate())
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt
new file mode 100644
index 0000000000..144422e618
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.CompoundMenuCandidate
+import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.NestedMenuCandidate
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MenuCandidateListAdapterTest {
+
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var dismiss: () -> Unit
+ private lateinit var reopenMenu: (NestedMenuCandidate) -> Unit
+ private lateinit var adapter: MenuCandidateListAdapter
+
+ @Before
+ fun setup() {
+ layoutInflater = spy(LayoutInflater.from(testContext))
+ dismiss = mock()
+ reopenMenu = mock()
+ adapter = MenuCandidateListAdapter(layoutInflater, dismiss, reopenMenu)
+ }
+
+ @Test
+ fun `items use layout resource as view type`() {
+ val items = listOf(
+ DecorativeTextMenuCandidate("one"),
+ TextMenuCandidate("two"),
+ CompoundMenuCandidate("three", false, end = CompoundMenuCandidate.ButtonType.CHECKBOX),
+ CompoundMenuCandidate("four", false, end = CompoundMenuCandidate.ButtonType.SWITCH),
+ DividerMenuCandidate(),
+ RowMenuCandidate(emptyList()),
+ )
+ adapter.submitList(items)
+
+ assertEquals(6, adapter.itemCount)
+ assertEquals(DecorativeTextMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(0))
+ assertEquals(TextMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(1))
+ assertEquals(CompoundCheckboxMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(2))
+ assertEquals(CompoundSwitchMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(3))
+ assertEquals(DividerMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(4))
+ assertEquals(RowMenuCandidateViewHolder.layoutResource, adapter.getItemViewType(5))
+ }
+
+ @Test
+ fun `bind will be forwarded to item implementation`() {
+ adapter.submitList(
+ listOf(
+ DividerMenuCandidate(),
+ ),
+ )
+
+ val holder: DividerMenuCandidateViewHolder = mock()
+
+ adapter.onBindViewHolder(holder, 0)
+
+ verify(holder).bind(DividerMenuCandidate())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..ca8a0fea9a
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.RowMenuCandidate
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class RowMenuCandidateViewHolderTest {
+
+ private lateinit var view: LinearLayout
+ private lateinit var button: AppCompatImageButton
+ private lateinit var inflater: LayoutInflater
+
+ @Before
+ fun setup() {
+ view = mock()
+ button = spy(AppCompatImageButton(testContext))
+ inflater = mock()
+
+ doReturn(button).`when`(inflater).inflate(
+ SmallMenuCandidateViewHolder.layoutResource,
+ view,
+ false,
+ )
+ }
+
+ @Test
+ fun `sets container state on view`() {
+ val holder = RowMenuCandidateViewHolder(view, inflater, mock())
+
+ holder.bind(RowMenuCandidate(emptyList()))
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ verify(view, never()).addView(button)
+ }
+
+ @Test
+ fun `creates buttons for small items`() {
+ val holder = RowMenuCandidateViewHolder(view, inflater, mock())
+
+ holder.bind(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate("hello", DrawableMenuIcon(null)),
+ SmallMenuCandidate("hello", DrawableMenuIcon(null)),
+ ),
+ ),
+ )
+ verify(view, times(2)).addView(button)
+
+ clearInvocations(view)
+
+ holder.bind(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate("test", DrawableMenuIcon(null)),
+ SmallMenuCandidate("hello", DrawableMenuIcon(null)),
+ ),
+ ),
+ )
+ verify(view, never()).removeAllViews()
+ verify(view, never()).addView(button)
+ }
+
+ @Test
+ fun `binds buttons for small items`() {
+ val holder = RowMenuCandidateViewHolder(view, inflater, mock())
+
+ holder.bind(
+ RowMenuCandidate(
+ listOf(
+ SmallMenuCandidate("hello", DrawableMenuIcon(null)),
+ ),
+ ),
+ )
+
+ verify(button).contentDescription = "hello"
+ verify(button).setImageDrawable(null)
+ verify(button).imageTintList = null
+ verify(button).visibility = View.VISIBLE
+ verify(button).isEnabled = true
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..f128297c2b
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageButton
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.SmallMenuCandidate
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SmallMenuCandidateViewHolderTest {
+
+ private lateinit var view: AppCompatImageButton
+
+ @Before
+ fun setup() {
+ view = spy(AppCompatImageButton(testContext))
+ }
+
+ @Test
+ fun `binds button data`() {
+ val holder = SmallMenuCandidateViewHolder(view, mock())
+
+ verify(view).setOnClickListener(holder)
+
+ holder.bind(SmallMenuCandidate("hello", DrawableMenuIcon(null)))
+
+ verify(view).contentDescription = "hello"
+ verify(view).setImageDrawable(null)
+ verify(view).imageTintList = null
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+
+ clearInvocations(view)
+
+ holder.bind(
+ SmallMenuCandidate(
+ "hello",
+ DrawableMenuIcon(null, tint = Color.BLUE),
+ ),
+ )
+ verify(view).setImageDrawable(null)
+ verify(view).imageTintList = ColorStateList.valueOf(Color.BLUE)
+ }
+
+ @Test
+ fun `sets on click listener`() {
+ var dismissed = false
+ var clicked = false
+ val holder = SmallMenuCandidateViewHolder(view) { dismissed = true }
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ holder.bind(SmallMenuCandidate("hello", DrawableMenuIcon(null)))
+ dismissed = false
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ dismissed = false
+ holder.bind(
+ SmallMenuCandidate(
+ "hello",
+ DrawableMenuIcon(null),
+ ) {
+ clicked = true
+ },
+ )
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertTrue(clicked)
+ }
+
+ @Test
+ fun `sets on long click listener`() {
+ var dismissed = false
+ var clicked = false
+ val holder = SmallMenuCandidateViewHolder(view) { dismissed = true }
+
+ holder.onLongClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ holder.bind(SmallMenuCandidate("hello", DrawableMenuIcon(null)))
+ verify(view).setOnClickListener(holder)
+ verify(view, atLeastOnce()).isLongClickable = false
+ dismissed = false
+
+ assertFalse(holder.onLongClick(null))
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ dismissed = false
+ holder.bind(
+ SmallMenuCandidate(
+ "hello",
+ DrawableMenuIcon(null),
+ onLongClick = {
+ clicked = true
+ true
+ },
+ ) {},
+ )
+ verify(view).isLongClickable = true
+
+ assertTrue(holder.onLongClick(null))
+ assertTrue(dismissed)
+ assertTrue(clicked)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt
new file mode 100644
index 0000000000..1c7f61fe25
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.Typeface
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class TextMenuCandidateViewHolderTest {
+
+ private lateinit var view: ConstraintLayout
+ private lateinit var textView: TextView
+
+ @Before
+ fun setup() {
+ val context: Context = mock()
+ view = mock()
+ textView = mock()
+
+ doReturn(context).`when`(view).context
+ doReturn(textView).`when`(view).findViewById<TextView>(R.id.label)
+
+ val resources: Resources = mock()
+ doReturn(resources).`when`(context).resources
+
+ doReturn(mock<Resources.Theme>()).`when`(context).theme
+ }
+
+ @Test
+ fun `sets container state and text on view`() {
+ val holder = TextMenuCandidateViewHolder(view, mock(), mock())
+
+ verify(view).setOnClickListener(holder)
+
+ holder.bind(TextMenuCandidate("hello"))
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = true
+ verify(textView).text = "hello"
+ verify(textView).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(textView).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+ }
+
+ @Test
+ fun `sets on click listener`() {
+ var dismissed = false
+ var clicked = false
+ val holder = TextMenuCandidateViewHolder(view, mock()) { dismissed = true }
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ holder.bind(TextMenuCandidate("hello"))
+ dismissed = false
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ dismissed = false
+ holder.bind(TextMenuCandidate("hello") { clicked = true })
+
+ holder.onClick(null)
+ assertTrue(dismissed)
+ assertTrue(clicked)
+ }
+
+ @Test
+ fun `sets highlight effect`() {
+ val holder = TextMenuCandidateViewHolder(view, mock(), mock())
+
+ holder.bind(TextMenuCandidate("hello"))
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view, never()).setBackgroundResource(anyInt())
+
+ holder.bind(
+ TextMenuCandidate(
+ "hello",
+ effect = HighPriorityHighlightEffect(Color.RED),
+ ),
+ )
+ verify(view).setBackgroundColor(Color.RED)
+ verify(view, never()).setBackgroundResource(anyInt())
+
+ clearInvocations(view)
+
+ holder.bind(
+ TextMenuCandidate(
+ "hello",
+ effect = null,
+ ),
+ )
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view).setBackgroundResource(anyInt())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt
new file mode 100644
index 0000000000..9df9c642e8
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt
@@ -0,0 +1,177 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter.icons
+
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableButtonMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class DrawableMenuIconViewHoldersTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ private lateinit var parent: ConstraintLayout
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var imageView: ImageView
+ private lateinit var imageButton: ImageButton
+
+ @Before
+ fun setup() {
+ parent = mock()
+ layoutInflater = mock()
+ imageView = mock()
+ imageButton = mock()
+
+ doReturn(testContext).`when`(parent).context
+ doReturn(testContext.resources).`when`(parent).resources
+ doReturn(imageView).`when`(layoutInflater).inflate(DrawableMenuIconViewHolder.layoutResource, parent, false)
+ doReturn(imageView).`when`(layoutInflater).inflate(AsyncDrawableMenuIconViewHolder.layoutResource, parent, false)
+ doReturn(imageButton).`when`(layoutInflater).inflate(DrawableButtonMenuIconViewHolder.layoutResource, parent, false)
+ doReturn(imageView).`when`(imageView).findViewById<TextView>(R.id.icon)
+ doReturn(imageButton).`when`(imageButton).findViewById<TextView>(R.id.icon)
+ }
+
+ @Test
+ fun `icon view holder sets icon on view`() {
+ val holder = DrawableMenuIconViewHolder(parent, layoutInflater, Side.END)
+
+ val drawable = mock<Drawable>()
+ holder.bindAndCast(DrawableMenuIcon(drawable), null)
+ verify(imageView).setImageDrawable(drawable)
+ verify(imageView).imageTintList = null
+ }
+
+ @Test
+ fun `button view holder sets icon on view`() {
+ val holder = DrawableButtonMenuIconViewHolder(parent, layoutInflater, Side.START) {}
+ verify(imageButton).setOnClickListener(holder)
+
+ val drawable = mock<Drawable>()
+ holder.bindAndCast(DrawableButtonMenuIcon(drawable), null)
+ verify(imageButton).setImageDrawable(drawable)
+ verify(imageButton).imageTintList = null
+ }
+
+ @Test
+ fun `async view holder sets icon on view`() = runTestOnMain {
+ val holder = AsyncDrawableMenuIconViewHolder(parent, layoutInflater, Side.END)
+
+ val drawable = mock<Drawable>()
+ holder.bindAndCast(AsyncDrawableMenuIcon(loadDrawable = { _, _ -> drawable }), null)
+ verify(imageView).setImageDrawable(null)
+ verify(imageView).setImageDrawable(drawable)
+ }
+
+ @Test
+ fun `async view holder uses loading icon and fallback icon`() = runTestOnMain {
+ val logger = mock<Logger>()
+ val holder = AsyncDrawableMenuIconViewHolder(parent, layoutInflater, Side.END, logger)
+
+ val loading = mock<Drawable>()
+ val fallback = mock<Drawable>()
+ holder.bindAndCast(
+ AsyncDrawableMenuIcon(
+ loadDrawable = { _, _ -> throw Exception() },
+ loadingDrawable = loading,
+ fallbackDrawable = fallback,
+ ),
+ null,
+ )
+ verify(imageView, never()).setImageDrawable(null)
+ verify(imageView).setImageDrawable(loading)
+ verify(imageView).setImageDrawable(fallback)
+ }
+
+ @Test
+ fun `icon holder removes image view on disconnect`() {
+ val holder = DrawableMenuIconViewHolder(parent, layoutInflater, Side.START)
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).addView(imageView)
+ clearInvocations(parent)
+
+ holder.disconnect()
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).removeView(imageView)
+ }
+
+ @Test
+ fun `button holder removes image view on disconnect`() {
+ val holder = DrawableButtonMenuIconViewHolder(parent, layoutInflater, Side.END) {}
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).addView(imageButton)
+ clearInvocations(parent)
+
+ holder.disconnect()
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).removeView(imageButton)
+ }
+
+ @Test
+ fun `async holder removes image view on disconnect`() {
+ val holder = AsyncDrawableMenuIconViewHolder(parent, layoutInflater, Side.START)
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).addView(imageView)
+ clearInvocations(parent)
+
+ holder.disconnect()
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).removeView(imageView)
+ }
+
+ @Test
+ fun `button view holder calls dismiss when clicked`() {
+ var dismissed = false
+ var clicked = false
+
+ val holder = DrawableButtonMenuIconViewHolder(parent, layoutInflater, Side.START) {
+ dismissed = true
+ }
+
+ holder.onClick(imageButton)
+ assertTrue(dismissed)
+ assertFalse(clicked)
+
+ val button = DrawableButtonMenuIcon(mock(), onClick = { clicked = true })
+ holder.bindAndCast(button, null)
+ holder.onClick(imageButton)
+ assertTrue(dismissed)
+ assertTrue(clicked)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt
new file mode 100644
index 0000000000..510389e995
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter.icons
+
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.DrawableButtonMenuIcon
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+class MenuIconAdapterTest {
+
+ private lateinit var layout: ConstraintLayout
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var dismiss: () -> Unit
+ private lateinit var adapter: MenuIconAdapter
+
+ @Before
+ fun setup() {
+ layout = mock()
+ layoutInflater = mock()
+ dismiss = mock()
+ adapter = spy(MenuIconAdapter(layout, layoutInflater, Side.START, dismiss))
+ }
+
+ @Test
+ fun `creates viewholder when icon type changes`() {
+ val mockViewHolder: MenuIconViewHolder<*> = mock()
+ doReturn(mockViewHolder).`when`(adapter).createViewHolder(any())
+
+ adapter.bind(null, null)
+ adapter.bind(TextMenuIcon("hello"), TextMenuIcon("world"))
+ adapter.bind(null, TextMenuIcon("world"))
+ verify(adapter, never()).createViewHolder(any())
+
+ adapter.bind(TextMenuIcon("hello"), null)
+ verify(adapter).createViewHolder(TextMenuIcon("hello"))
+
+ clearInvocations(adapter)
+ adapter.bind(TextMenuIcon("hello"), DrawableMenuIcon(mock()))
+ verify(adapter).createViewHolder(TextMenuIcon("hello"))
+ }
+
+ @Test
+ fun `disconnects viewholder when icon is changed`() {
+ val mockViewHolder: MenuIconViewHolder<*> = mock()
+ doReturn(mockViewHolder).`when`(adapter).createViewHolder(any())
+ adapter.bind(TextMenuIcon("hello"), null)
+
+ verify(mockViewHolder, never()).disconnect()
+ adapter.bind(DrawableButtonMenuIcon(mock()), TextMenuIcon("hello"))
+ verify(mockViewHolder).disconnect()
+
+ clearInvocations(mockViewHolder)
+ adapter.bind(null, DrawableButtonMenuIcon(mock()))
+ verify(mockViewHolder).disconnect()
+ }
+
+ @Test
+ fun `always bind new icon`() {
+ val mockViewHolder: MenuIconViewHolder<*> = mock()
+ doReturn(mockViewHolder).`when`(adapter).createViewHolder(any())
+
+ adapter.bind(TextMenuIcon("hello"), null)
+ verify(mockViewHolder).bindAndCast(TextMenuIcon("hello"), null)
+
+ adapter.bind(TextMenuIcon("hello"), TextMenuIcon("hello"))
+ verify(mockViewHolder).bindAndCast(TextMenuIcon("hello"), TextMenuIcon("hello"))
+
+ clearInvocations(mockViewHolder)
+ adapter.bind(null, TextMenuIcon("hello"))
+ verify(mockViewHolder, never()).bindAndCast(any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt
new file mode 100644
index 0000000000..bdba1c20f7
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.adapter.icons
+
+import android.graphics.Typeface
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu2.R
+import mozilla.components.concept.menu.Side
+import mozilla.components.concept.menu.candidate.TextMenuIcon
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class TextMenuIconViewHolderTest {
+
+ private lateinit var parent: ConstraintLayout
+ private lateinit var layoutInflater: LayoutInflater
+ private lateinit var textView: TextView
+
+ @Before
+ fun setup() {
+ parent = mock()
+ layoutInflater = mock()
+ textView = mock()
+
+ doReturn(testContext).`when`(parent).context
+ doReturn(textView).`when`(layoutInflater).inflate(TextMenuIconViewHolder.layoutResource, parent, false)
+ doReturn(textView).`when`(textView).findViewById<TextView>(R.id.icon)
+ }
+
+ @Test
+ fun `sets container state and text on view`() {
+ val holder = TextMenuIconViewHolder(parent, layoutInflater, Side.START)
+
+ holder.bindAndCast(TextMenuIcon("hello"), null)
+ verify(textView).text = "hello"
+ verify(textView).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(textView).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+ }
+
+ @Test
+ fun `removes text view on disconnect`() {
+ val holder = TextMenuIconViewHolder(parent, layoutInflater, Side.END)
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).addView(textView)
+ clearInvocations(parent)
+
+ holder.disconnect()
+
+ verify(parent).setConstraintSet(any())
+ verify(parent).removeView(textView)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt
new file mode 100644
index 0000000000..d9be746793
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt
@@ -0,0 +1,856 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.ext
+
+import android.graphics.Rect
+import android.view.View
+import android.widget.PopupWindow
+import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.menu2.R
+import mozilla.components.browser.menu2.adapter.MenuCandidateListAdapter
+import mozilla.components.concept.menu.MenuStyle
+import mozilla.components.concept.menu.Orientation
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when`
+import kotlin.math.roundToInt
+
+/**
+ * [PopupWindow] const's.
+ */
+private const val HALF_MENU_ITEM = 0.5
+
+/**
+ * [PopupWindow] UI components.
+ */
+private const val SCREEN_ROOT_VIEW_HEIGHT = 1000
+private const val SCREEN_ROOT_VIEW_WIDTH = 400
+private const val MENU_ITEM_HEIGHT = 50
+private const val DEFAULT_ITEM_COUNT = 10
+private const val MENU_CONTAINER_WIDTH = 100
+private const val MENU_CONTAINER_PADDING = 10
+
+@RunWith(AndroidJUnit4::class)
+class BrowserMenuPositioningTest {
+ private lateinit var popupWindow: PopupWindow
+ private lateinit var overlapStyle: MenuStyle
+ private lateinit var offsetStyle: MenuStyle
+ private lateinit var offsetOverlapStyle: MenuStyle
+
+ @Before
+ fun setUp() {
+ overlapStyle = MenuStyle(completelyOverlap = true)
+ offsetStyle = MenuStyle(horizontalOffset = 10, verticalOffset = 10)
+ offsetOverlapStyle =
+ MenuStyle(completelyOverlap = true, horizontalOffset = 10, verticalOffset = 10)
+
+ popupWindow = spy(PopupWindow())
+ }
+
+ @Test
+ fun `WHEN recycler view has no adapter THEN menu positioning data is null`() {
+ val containerView = createContainerView(hasAdapter = false)
+ val anchor = mock(View::class.java)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN container view has no measured height THEN menu positioning data is null`() {
+ val containerView = createContainerView()
+ val anchor = mock(View::class.java)
+
+ `when`(containerView.measuredHeight).thenReturn(0)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN container view has no measured width THEN menu positioning data is null`() {
+ val containerView = createContainerView()
+ val anchor = mock(View::class.java)
+
+ `when`(containerView.measuredWidth).thenReturn(0)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN recycler view has no measured height THEN menu positioning data is null`() {
+ val containerView = createContainerView(recyclerViewHeight = 0)
+ val anchor = mock(View::class.java)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN recycler view has no items THEN menu positioning data is null`() {
+ val containerView = createContainerView(recyclerViewHeight = 100, itemCount = 0)
+ val anchor = mock(View::class.java)
+
+ assertNull(inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP))
+ }
+
+ @Test
+ fun `WHEN orientation up & fits available height THEN original height & positioned from the bottom of the anchor with leftBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation up & fits available height & is RTL THEN original height & positioned from the bottom right of the anchor with rightBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation up & does not fit available height & fits down THEN original height & positioned from the top left of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation up & does not fit up or down THEN original height & positioned from the top left of the anchor with leftAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = 20 to notEnoughHeight
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation up & does not fit up or down & is RTL THEN original height & positioned from the bottom right of the anchor with rightAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = SCREEN_ROOT_VIEW_HEIGHT - 20 to notEnoughHeight
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation down & fits available height THEN original height & positioned from the top left of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation down & fits available height & is RTL THEN original height & positioned from the top right of the anchor with rightTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = Orientation.DOWN)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN number of menu items don't fit available height THEN set popup with calculated height`() {
+ val containerView = createContainerView(itemCount = 22)
+ val anchor = createAnchor(20, 25)
+
+ val positioningData = inferMenuPositioningData(containerView, anchor, orientation = Orientation.UP)
+
+ val availableHeight = anchor.rootView.measuredHeight
+ val maxAvailableHeightForRecyclerView = availableHeight - MENU_CONTAINER_PADDING
+ val numberOfItemsFitExactly =
+ (maxAvailableHeightForRecyclerView.toFloat() / MENU_ITEM_HEIGHT.toFloat()).roundToInt()
+ val numberOfItemsFitWithOverFlow = numberOfItemsFitExactly - HALF_MENU_ITEM
+ val updatedRecyclerViewHeight = (numberOfItemsFitWithOverFlow * MENU_ITEM_HEIGHT).toInt()
+ val calculatedHeight = updatedRecyclerViewHeight + MENU_CONTAINER_PADDING
+
+ assertEquals(calculatedHeight, positioningData?.containerHeight)
+ }
+
+ @Test
+ fun `WHEN orientation is null & menu fits down THEN original height & positioned from the top left of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = null)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN orientation is null & menu does not fit & is RTL THEN showAtLocation with original height & positioned from the top right of the anchor with rightAnimation`() {
+ val containerView = createContainerView(itemCount = 20)
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, orientation = null)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, directionRight = false, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation up & fits up THEN original height & bottom of the menu positioned exactly to the bottom of the anchor with leftBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation up & fits up & is RTL THEN original height & bottom right of the menu positioned exactly to the bottom right of the anchor with rightBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation down & fits down THEN showAtLocation with original height & positioned exactly from the top of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.DOWN)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation down & fits down & is RTL THEN original height & positioned exactly from the top of the anchor with rightTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.DOWN)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation up & does not fit up & fits down THEN original height & positioned exactly from the top left of the anchor with leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should completely overlap & orientation up & does not fit up or down THEN original height & positioned exactly from the top left of the anchor with leftAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = 20 to notEnoughHeight
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, overlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, overlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation up & fits up THEN original height & positioned from the bottom left of the anchor with applied offsets and leftBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation up & fits up & is RTL THEN original height & positioned from the bottom right of the anchor with applied offsets and rightBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation down & fits down THEN original height & positioned from the top left of the anchor with applied offsets and leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation down & fits down & is RTL THEN original height & positioned from the top right of the anchor with applied offsets and rightTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation up & does not fit up & fits down THEN original height & positioned from the top left of the anchor with applied offsets and leftAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN has style offsets & orientation up & does not fit up or down THEN original height & positioned from the top left of the anchor with applied offsets leftAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = 20 to notEnoughHeight
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation up & fits up THEN original height & positioned exactly from the bottom left of the anchor with applied offsets leftBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation up & fits up & is RTL THEN original height & positioned exactly to the bottom right of the anchor with applied offsets rightBottomAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to SCREEN_ROOT_VIEW_HEIGHT - 20
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation down & fits down THEN original height & positioned exactly from the top of the anchor with applied offsets leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation down & fits down & is RTL THEN original height & positioned exactly from the top of the anchor with applied offsets and rightTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = SCREEN_ROOT_VIEW_WIDTH - 20 to 25
+ val anchor = createAnchor(x, y, true)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false, directionRight = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation up & fits down only THEN original height & positioned exactly from the top left of the anchor with applied offsets and leftTopAnimation`() {
+ val containerView = createContainerView()
+
+ val (x, y) = 20 to 25
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop,
+ )
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `WHEN should overlap & has style offsets & orientation up & does not fit up or down THEN original height & positioned exactly from the top left of the anchor with applied offsets and leftAnimation`() {
+ val containerView = createContainerView()
+
+ val notEnoughHeight = containerView.measuredHeight.minus(MENU_CONTAINER_PADDING).minus(1)
+ val (x, y) = 20 to notEnoughHeight
+ val anchor = createAnchor(x, y)
+
+ val result = inferMenuPositioningData(containerView, anchor, offsetOverlapStyle, Orientation.UP)
+
+ val (targetX, targetY) = getTargetCoordinates(x, y, containerView, anchor, offsetOverlapStyle, directionUp = false)
+ val expected = MenuPositioningData(
+ anchor = anchor,
+ x = targetX,
+ y = targetY,
+ containerHeight = containerView.measuredHeight,
+ animation = R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft,
+ )
+
+ assertEquals(expected, result)
+ }
+}
+
+internal fun createContainerView(
+ itemCount: Int = DEFAULT_ITEM_COUNT,
+ hasAdapter: Boolean = true,
+ recyclerViewHeight: Int? = null,
+): View {
+ val containerView = mock(View::class.java)
+
+ // Mimicking elevation added spacing.
+ val recyclerView = createRecyclerView(itemCount, hasAdapter, recyclerViewHeight)
+ val containerHeight = recyclerView.measuredHeight.plus(MENU_CONTAINER_PADDING)
+ val containerWidth = recyclerView.measuredWidth.plus(MENU_CONTAINER_PADDING)
+ `when`(containerView.measuredHeight).thenReturn(containerHeight)
+ `when`(containerView.measuredWidth).thenReturn(containerWidth)
+
+ doReturn(recyclerView).`when`(containerView)
+ .findViewById<RecyclerView>(R.id.mozac_browser_menu_recyclerView)
+ return containerView
+}
+
+private fun createRecyclerView(
+ itemCount: Int = DEFAULT_ITEM_COUNT,
+ hasAdapter: Boolean = true,
+ recyclerViewHeight: Int? = null,
+): RecyclerView {
+ val recyclerView = mock(RecyclerView::class.java)
+
+ if (hasAdapter) {
+ val adapter = mock(MenuCandidateListAdapter::class.java)
+ `when`(adapter.itemCount).thenReturn(itemCount)
+ `when`(recyclerView.adapter).thenReturn(adapter)
+ }
+
+ `when`(recyclerView.measuredHeight).thenReturn(
+ recyclerViewHeight
+ ?: (itemCount * MENU_ITEM_HEIGHT),
+ )
+
+ `when`(recyclerView.measuredWidth).thenReturn(MENU_CONTAINER_WIDTH)
+
+ return recyclerView
+}
+
+internal fun createAnchor(x: Int, y: Int, isRTL: Boolean = false): View {
+ val view = spy(View(testContext))
+
+ doAnswer { invocation ->
+ val locationOnScreen = (invocation.getArgument(0) as IntArray)
+ locationOnScreen[0] = x
+ locationOnScreen[1] = y
+ locationOnScreen
+ }.`when`(view).getLocationOnScreen(any())
+
+ doAnswer { invocation ->
+ val locationInWindow = (invocation.getArgument(0) as IntArray)
+ locationInWindow[0] = x
+ locationInWindow[1] = y
+ locationInWindow
+ }.`when`(view).getLocationInWindow(any())
+
+ if (isRTL) {
+ doReturn(ViewCompat.LAYOUT_DIRECTION_RTL).`when`(view).layoutDirection
+ } else {
+ doReturn(ViewCompat.LAYOUT_DIRECTION_LTR).`when`(view).layoutDirection
+ }
+ doReturn(10).`when`(view).height
+ doReturn(15).`when`(view).width
+
+ val anchorRootView = createAnchorRootView()
+ `when`(view.rootView).thenReturn(anchorRootView)
+
+ return view
+}
+
+private fun createAnchorRootView(): View {
+ val view = spy(View(testContext))
+ doAnswer { invocation ->
+ val displayFrame = (invocation.getArgument(0) as Rect)
+ displayFrame.left = 0
+ displayFrame.right = SCREEN_ROOT_VIEW_WIDTH
+ displayFrame.bottom = SCREEN_ROOT_VIEW_HEIGHT
+ displayFrame
+ }.`when`(view).getWindowVisibleDisplayFrame(any())
+
+ `when`(view.measuredHeight).thenReturn(SCREEN_ROOT_VIEW_HEIGHT)
+
+ return view
+}
+
+internal fun getTargetCoordinates(
+ anchorX: Int,
+ anchorY: Int,
+ containerView: View,
+ anchor: View,
+ style: MenuStyle? = null,
+ directionUp: Boolean = true,
+ directionRight: Boolean = true,
+): Pair<Int, Int> {
+ val targetX = getTargetX(
+ anchorX,
+ containerView,
+ anchor,
+ directionRight,
+ style?.completelyOverlap ?: false,
+ style?.horizontalOffset ?: 0,
+ )
+ val targetY = getTargetY(
+ anchorY,
+ containerView,
+ anchor,
+ directionUp,
+ style?.completelyOverlap ?: false,
+ style?.verticalOffset ?: 0,
+ )
+ return targetX to targetY
+}
+
+private fun getTargetX(
+ anchorX: Int,
+ containerView: View,
+ anchor: View,
+ directionRight: Boolean,
+ shouldOverlap: Boolean,
+ horizontalOffset: Int,
+): Int {
+ val targetX = when {
+ directionRight && shouldOverlap -> anchorX - (MENU_CONTAINER_PADDING / 2)
+ directionRight && !shouldOverlap -> anchorX
+ !directionRight && shouldOverlap -> anchorX - (containerView.measuredWidth - anchor.width) + (MENU_CONTAINER_PADDING / 2)
+ else -> anchorX - (containerView.measuredWidth - anchor.width)
+ }
+
+ return if (directionRight) {
+ targetX + horizontalOffset
+ } else {
+ targetX - horizontalOffset
+ }
+}
+
+private fun getTargetY(
+ anchorY: Int,
+ containerView: View,
+ anchor: View,
+ directionUp: Boolean,
+ shouldOverlap: Boolean,
+ verticalOffset: Int,
+): Int {
+ val targetY = when {
+ directionUp && shouldOverlap -> anchorY - (containerView.measuredHeight - anchor.height) + (MENU_CONTAINER_PADDING / 2)
+ directionUp && !shouldOverlap -> anchorY - (containerView.measuredHeight - anchor.height)
+ !directionUp && shouldOverlap -> anchorY - (MENU_CONTAINER_PADDING / 2)
+ else -> anchorY
+ }
+
+ return if (directionUp) {
+ targetY - verticalOffset
+ } else {
+ targetY + verticalOffset
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt
new file mode 100644
index 0000000000..6b5e406bd5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.ext
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.candidate.ContainerStyle
+import mozilla.components.concept.menu.candidate.DrawableMenuIcon
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.TextStyle
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ViewTest {
+
+ @Test
+ fun `apply container style affects visible and enabled`() {
+ val view: View = mock()
+ view.applyStyle(ContainerStyle(), ContainerStyle())
+ verify(view, never()).visibility = View.VISIBLE
+ verify(view, never()).isEnabled = true
+
+ view.applyStyle(ContainerStyle(isVisible = false), ContainerStyle())
+ verify(view).visibility = View.GONE
+ verify(view).isEnabled = true
+
+ view.applyStyle(ContainerStyle(isEnabled = false), ContainerStyle())
+ verify(view).visibility = View.VISIBLE
+ verify(view).isEnabled = false
+ }
+
+ @Test
+ fun `apply text style affects text styling`() {
+ val view: TextView = mock()
+ view.applyStyle(TextStyle(), TextStyle())
+ verify(view, never()).textSize = anyFloat()
+ verify(view, never()).setTextColor(anyInt())
+ verify(view, never()).setTypeface(any(), anyInt())
+ verify(view, never()).textAlignment = anyInt()
+
+ view.applyStyle(TextStyle(), TextStyle(size = 0f))
+ verify(view, never()).textSize = anyFloat()
+ verify(view, never()).setTextColor(anyInt())
+ verify(view).setTypeface(any(), eq(Typeface.NORMAL))
+ verify(view).textAlignment = View.TEXT_ALIGNMENT_INHERIT
+
+ view.applyStyle(
+ TextStyle(
+ size = 4f,
+ color = Color.RED,
+ textStyle = Typeface.ITALIC,
+ textAlignment = View.TEXT_ALIGNMENT_CENTER,
+ ),
+ TextStyle(),
+ )
+ verify(view).textSize = 4f
+ verify(view).setTextColor(Color.RED)
+ verify(view).setTypeface(any(), eq(Typeface.ITALIC))
+ verify(view).textAlignment = View.TEXT_ALIGNMENT_CENTER
+ }
+
+ @Test
+ fun `apply drawable icon`() {
+ val view: ImageView = mock()
+ view.applyIcon(DrawableMenuIcon(null), DrawableMenuIcon(null))
+ verify(view, never()).setImageDrawable(any())
+ verify(view, never()).imageTintList = any()
+
+ val drawable: Drawable = mock()
+ view.applyIcon(DrawableMenuIcon(drawable), DrawableMenuIcon(null))
+ verify(view).setImageDrawable(drawable)
+ verify(view).imageTintList = null
+
+ view.applyIcon(DrawableMenuIcon(null, Color.RED), DrawableMenuIcon(drawable))
+ verify(view).setImageDrawable(drawable)
+ verify(view).imageTintList = ColorStateList.valueOf(Color.RED)
+ }
+
+ @Test
+ fun `apply notification effect`() {
+ val view: ImageView = mock()
+ view.applyNotificationEffect(LowPriorityHighlightEffect(Color.BLUE), LowPriorityHighlightEffect(Color.BLUE))
+ view.applyNotificationEffect(null, null)
+ verify(view, never()).visibility = anyInt()
+ verify(view, never()).imageTintList = any()
+
+ view.applyNotificationEffect(LowPriorityHighlightEffect(Color.BLUE), null)
+ verify(view).visibility = View.VISIBLE
+ verify(view).imageTintList = ColorStateList.valueOf(Color.BLUE)
+
+ view.applyNotificationEffect(null, LowPriorityHighlightEffect(Color.BLUE))
+ verify(view).visibility = View.GONE
+ verify(view).imageTintList = null
+ }
+
+ @Test
+ fun `sets highlight effect`() {
+ val view: View = mock()
+ doReturn(testContext).`when`(view).context
+
+ view.applyBackgroundEffect(null, null)
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view, never()).setBackgroundResource(anyInt())
+ verify(view, never()).foreground = any()
+
+ view.applyBackgroundEffect(HighPriorityHighlightEffect(Color.RED), HighPriorityHighlightEffect(Color.RED))
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view, never()).setBackgroundResource(anyInt())
+ verify(view, never()).foreground = any()
+
+ view.applyBackgroundEffect(HighPriorityHighlightEffect(Color.RED), null)
+ verify(view).setBackgroundColor(Color.RED)
+ verify(view, never()).setBackgroundResource(anyInt())
+ verify(view).foreground = any()
+
+ clearInvocations(view)
+
+ view.applyBackgroundEffect(null, HighPriorityHighlightEffect(Color.RED))
+ verify(view, never()).setBackgroundColor(anyInt())
+ verify(view).setBackgroundResource(anyInt())
+ verify(view).foreground = null
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt
new file mode 100644
index 0000000000..29874f78ff
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.view
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.widget.ImageView
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.menu.MenuController
+import mozilla.components.concept.menu.candidate.HighPriorityHighlightEffect
+import mozilla.components.concept.menu.candidate.LowPriorityHighlightEffect
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MenuButton2Test {
+ private lateinit var menuController: MenuController
+ private lateinit var menuButton: MenuButton2
+ private lateinit var menuIcon: ImageView
+ private lateinit var highlightView: ImageView
+ private lateinit var notificationIconView: ImageView
+
+ @Before
+ fun setup() {
+ menuController = mock()
+ menuButton = MenuButton2(testContext)
+
+ val images = menuButton.children.mapNotNull { it as? AppCompatImageView }.toList()
+ highlightView = images[0]
+ menuIcon = images[1]
+ notificationIconView = images[2]
+ }
+
+ @Test
+ fun `changing menu controller dismisses old menu`() {
+ menuButton.menuController = menuController
+ menuButton.performClick()
+
+ verify(menuController).register(any(), eq(menuButton))
+ verify(menuController).show(menuButton)
+
+ menuButton.menuController = mock()
+ verify(menuController).dismiss()
+ verify(menuController).unregister(any())
+ }
+
+ @Test
+ fun `changing menu controller to null dismisses old menu`() {
+ menuButton.menuController = menuController
+ menuButton.performClick()
+
+ verify(menuController).register(any(), eq(menuButton))
+
+ menuButton.menuController = null
+ verify(menuController).dismiss()
+ verify(menuController).unregister(any())
+ }
+
+ @Test
+ fun `icon has content description`() {
+ assertEquals("Menu", menuIcon.contentDescription)
+ assertNotNull(menuIcon.drawable)
+ }
+
+ @Test
+ fun `icon color filter can be changed`() {
+ assertNull(menuIcon.colorFilter)
+
+ menuButton.setColorFilter(0xffffff)
+ assertEquals(PorterDuffColorFilter(0xffffff, PorterDuff.Mode.SRC_ATOP), menuIcon.colorFilter)
+ }
+
+ @Test
+ fun `icon displays high priority highlight`() {
+ assertFalse(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ menuButton.setEffect(
+ HighPriorityHighlightEffect(Color.RED),
+ )
+
+ assertTrue(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ assertEquals(ColorStateList.valueOf(Color.RED), highlightView.imageTintList)
+ }
+
+ @Test
+ fun `icon displays low priority highlight`() {
+ assertFalse(highlightView.isVisible)
+ assertFalse(notificationIconView.isVisible)
+
+ menuButton.setEffect(
+ LowPriorityHighlightEffect(Color.BLUE),
+ )
+
+ assertFalse(highlightView.isVisible)
+ assertTrue(notificationIconView.isVisible)
+
+ assertEquals(PorterDuffColorFilter(Color.BLUE, PorterDuff.Mode.SRC_ATOP), notificationIconView.colorFilter)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt
new file mode 100644
index 0000000000..5f39e68c56
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.browser.menu2.view
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Build
+import androidx.cardview.widget.CardView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+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
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class MenuViewTest {
+
+ private val items = listOf(
+ DecorativeTextMenuCandidate("Hello"),
+ DecorativeTextMenuCandidate("World"),
+ )
+ private lateinit var menuView: MenuView
+ private lateinit var cardView: CardView
+ private lateinit var recyclerView: RecyclerView
+
+ @Before
+ fun setup() {
+ menuView = spy(MenuView(testContext))
+ cardView = menuView.findViewById(R.id.mozac_browser_menu_cardView)
+ recyclerView = menuView.findViewById(R.id.mozac_browser_menu_recyclerView)
+ }
+
+ @Test
+ fun `recyclerview adapter will have items for every menu item`() {
+ assertNotNull(recyclerView)
+
+ val recyclerAdapter = recyclerView.adapter!!
+ assertNotNull(recyclerAdapter)
+ assertEquals(0, recyclerAdapter.itemCount)
+
+ menuView.submitList(items)
+ assertEquals(2, recyclerAdapter.itemCount)
+ }
+
+ @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())
+
+ menuView.setVisibleSide(Side.END)
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+
+ assertFalse(layoutManager.stackFromEnd)
+ verify(menuView).scrollOnceToTheBottom(any())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `setVisibleSide changes stackFromEnd on devices with Android N and above`() {
+ doNothing().`when`(menuView).scrollOnceToTheBottom(any())
+
+ menuView.setVisibleSide(Side.END)
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+
+ assertTrue(layoutManager.stackFromEnd)
+ verify(menuView, never()).scrollOnceToTheBottom(any())
+ }
+
+ @Test
+ fun `setStyle changes background color`() {
+ menuView.setStyle(MenuStyle(backgroundColor = Color.BLUE))
+ assertEquals(ColorStateList.valueOf(Color.BLUE), cardView.cardBackgroundColor)
+ }
+}
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..49324d83c5
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,3 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
+
diff --git a/mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties b/mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28