From 9d8a629672312a40b6d0d09a3b875dad1426664e Mon Sep 17 00:00:00 2001
From: nkdengineer <nkdengineer@outlook.com>
Date: Mon, 26 Jan 2026 16:56:17 +0700
Subject: [PATCH 1/4] fix: The icon of link opening new page/tab is not
 announced

---
 src/components/MenuItem.tsx | 19 ++++++++++++++++---
 src/languages/de.ts         |  1 +
 src/languages/en.ts         |  1 +
 src/languages/es.ts         |  1 +
 src/languages/fr.ts         |  1 +
 src/languages/it.ts         |  1 +
 src/languages/ja.ts         |  1 +
 src/languages/nl.ts         |  1 +
 src/languages/pl.ts         |  1 +
 src/languages/pt-BR.ts      |  1 +
 src/languages/zh-hans.ts    |  1 +
 11 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 23d5d73eb3d2..00be9ad07831 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -5,6 +5,7 @@ import type {GestureResponderEvent, Role, StyleProp, TextStyle, ViewStyle} from
 import {View} from 'react-native';
 import type {ValueOf} from 'type-fest';
 import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useTheme from '@hooks/useTheme';
@@ -558,7 +559,7 @@ function MenuItem({
     shouldBeAccessible = true,
     tabIndex = 0,
 }: MenuItemProps) {
-    const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'FallbackAvatar', 'DotIndicator', 'Checkmark']);
+    const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'FallbackAvatar', 'DotIndicator', 'Checkmark', 'NewWindow']);
     const theme = useTheme();
     const styles = useThemeStyles();
     const StyleUtils = useStyleUtils();
@@ -567,7 +568,7 @@ function MenuItem({
     const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {};
     const popoverAnchor = useRef<View>(null);
     const deviceHasHoverSupport = hasHoverSupport();
-
+    const {translate} = useLocalize();
     const isCompact = viewMode === CONST.OPTION_MODE.COMPACT;
     const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedbackDeleted) : false;
     const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1;
@@ -708,6 +709,18 @@ function MenuItem({
 
     const isIDPassed = !!iconReportID || !!iconAccountID || iconAccountID === CONST.DEFAULT_NUMBER_ID;
 
+    const isNewWindowIcon = useMemo(() => {
+        return iconRight === icons.NewWindow;
+    }, [iconRight, icons.NewWindow]);
+
+    const enhancedAccessibilityLabel = useMemo(() => {
+        const result = accessibilityLabel ?? defaultAccessibilityLabel;
+        if (isNewWindowIcon) {
+            return `${result}. ${translate('common.opensInNewTab')}`;
+        }
+        return result;
+    }, [accessibilityLabel, defaultAccessibilityLabel, isNewWindowIcon, translate]);
+
     return (
         <View
             style={rootWrapperStyle}
@@ -759,7 +772,7 @@ function MenuItem({
                                 disabled={disabled || isExecuting}
                                 ref={mergeRefs(ref, popoverAnchor)}
                                 role={role}
-                                accessibilityLabel={accessibilityLabel ?? defaultAccessibilityLabel}
+                                accessibilityLabel={enhancedAccessibilityLabel}
                                 accessible={shouldBeAccessible}
                                 tabIndex={tabIndex}
                                 onFocus={onFocus}
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 336bf871e906..2be6d7e02d69 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -633,6 +633,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Wechselkurs',
         reimbursableTotal: 'Erstattungsfähiger Gesamtbetrag',
         nonReimbursableTotal: 'Nicht erstattungsfähiger Gesamtbetrag',
+        opensInNewTab: 'Öffnet sich in einem neuen Tab',
         originalAmount: 'Ursprünglicher Betrag',
         insights: 'Einblicke',
         duplicateExpense: 'Doppelte Ausgabe',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 200e8335566f..e08edd14c04a 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -628,6 +628,7 @@ const translations = {
         exchangeRate: 'Exchange rate',
         reimbursableTotal: 'Reimbursable total',
         nonReimbursableTotal: 'Non-reimbursable total',
+        opensInNewTab: 'Opens in a new tab',
     },
     supportalNoAccess: {
         title: 'Not so fast',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index a988d3c80d67..23bd46c074ac 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -387,6 +387,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Tipo de cambio',
         reimbursableTotal: 'Total reembolsable',
         nonReimbursableTotal: 'Total no reembolsable',
+        opensInNewTab: 'Se abre en una nueva pestaña',
     },
     supportalNoAccess: {
         title: 'No tan rápido',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index c80f7a1d5879..b37ded2ad213 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -635,6 +635,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Taux de change',
         reimbursableTotal: 'Total remboursable',
         nonReimbursableTotal: 'Total non remboursable',
+        opensInNewTab: 'S’ouvre dans un nouvel onglet',
         originalAmount: 'Montant d’origine',
         insights: 'Analyses',
         duplicateExpense: 'Note de frais en double',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index c466864413ba..7e242b134fd8 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -634,6 +634,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Tasso di cambio',
         reimbursableTotal: 'Totale rimborsabile',
         nonReimbursableTotal: 'Totale non rimborsabile',
+        opensInNewTab: 'Si apre in una nuova scheda',
         originalAmount: 'Importo originale',
         insights: 'Analisi',
         duplicateExpense: 'Spesa duplicata',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index cbe74fca2668..cfc7f3ffe27c 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -633,6 +633,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: '為替レート',
         reimbursableTotal: '経費精算対象の合計',
         nonReimbursableTotal: '非払い戻し合計',
+        opensInNewTab: '新しいタブで開きます',
         originalAmount: '元の金額',
         insights: 'インサイト',
         duplicateExpense: '重複した経費',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index d07a6f98c087..efbe9e39bf8a 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -634,6 +634,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Wisselkoers',
         reimbursableTotal: 'Totaal te vergoeden',
         nonReimbursableTotal: 'Niet-vergoedbaar totaal',
+        opensInNewTab: 'Opent in een nieuw tabblad',
         originalAmount: 'Oorspronkelijk bedrag',
         insights: 'Inzichten',
         duplicateExpense: 'Dubbele uitgave',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 08e2d8bb294c..c484b8c520cf 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -634,6 +634,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Kurs wymiany',
         reimbursableTotal: 'Łączna kwota podlegająca zwrotowi',
         nonReimbursableTotal: 'Suma niepodlegająca zwrotowi',
+        opensInNewTab: 'Otwiera się w nowej karcie',
         originalAmount: 'Kwota pierwotna',
         insights: 'Analizy',
         duplicateExpense: 'Zduplikowany wydatek',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index cfcc64ebc274..45535698f8aa 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -633,6 +633,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Taxa de câmbio',
         reimbursableTotal: 'Total reembolsável',
         nonReimbursableTotal: 'Total não reembolsável',
+        opensInNewTab: 'Abre em uma nova aba',
         originalAmount: 'Valor original',
         insights: 'Insights',
         duplicateExpense: 'Despesa duplicada',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index c38247687c9a..7a63a9449f45 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -630,6 +630,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: '汇率',
         reimbursableTotal: '可报销总额',
         nonReimbursableTotal: '不可报销总额',
+        opensInNewTab: '在新标签页中打开',
         originalAmount: '原始金额',
         insights: '洞察',
         duplicateExpense: '重复报销',

From c75936970ac38136196df0e2984d1009860fe394 Mon Sep 17 00:00:00 2001
From: nkdengineer <nkdengineer@outlook.com>
Date: Fri, 30 Jan 2026 21:55:39 +0700
Subject: [PATCH 2/4] remove useMemo

---
 src/components/MenuItem.tsx | 16 +++++-----------
 1 file changed, 5 insertions(+), 11 deletions(-)

diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 50431dc4abba..bbaa901ecf77 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -713,17 +713,11 @@ function MenuItem({
 
     const isIDPassed = !!iconReportID || !!iconAccountID || iconAccountID === CONST.DEFAULT_NUMBER_ID;
 
-    const isNewWindowIcon = useMemo(() => {
-        return iconRight === icons.NewWindow;
-    }, [iconRight, icons.NewWindow]);
-
-    const enhancedAccessibilityLabel = useMemo(() => {
-        const result = accessibilityLabel ?? defaultAccessibilityLabel;
-        if (isNewWindowIcon) {
-            return `${result}. ${translate('common.opensInNewTab')}`;
-        }
-        return result;
-    }, [accessibilityLabel, defaultAccessibilityLabel, isNewWindowIcon, translate]);
+    const isNewWindowIcon = iconRight === icons.NewWindow;
+    let enhancedAccessibilityLabel = accessibilityLabel ?? defaultAccessibilityLabel;
+    if (isNewWindowIcon) {
+        enhancedAccessibilityLabel = `${enhancedAccessibilityLabel}. ${translate('common.opensInNewTab')}`;
+    }
 
     return (
         <View

From fca169c20448bdbf1d2a64dc7367c811fd324163 Mon Sep 17 00:00:00 2001
From: nkdengineer <nkdengineer@outlook.com>
Date: Wed, 11 Feb 2026 15:33:13 +0700
Subject: [PATCH 3/4] complete resolve conflict

---
 src/languages/de.ts    | 2 +-
 src/languages/pt-BR.ts | 3 ---
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/languages/de.ts b/src/languages/de.ts
index 5eeb0bc24842..ed5313fd2b3e 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -592,7 +592,7 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Wechselkurs',
         reimbursableTotal: 'Erstattungsfähiger Gesamtbetrag',
         nonReimbursableTotal: 'Nicht erstattungsfähige Gesamtsumme',
-        opensInNewTab: 'Öffnet sich in einem neuen Tab',
+        opensInNewTab: 'Wird in einem neuen Tab geöffnet',
         locked: 'Gesperrt',
         month: 'Monat',
         week: 'Woche',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index a6ebff5fc771..feb4c28ba155 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -591,11 +591,8 @@ const translations: TranslationDeepObject<typeof en> = {
         exchangeRate: 'Taxa de câmbio',
         reimbursableTotal: 'Total reembolsável',
         nonReimbursableTotal: 'Total não reembolsável',
-<<<<<<< HEAD
         opensInNewTab: 'Abre em uma nova aba',
-=======
         locked: 'Bloqueado',
->>>>>>> main
         month: 'Mês',
         week: 'Semana',
         year: 'Ano',

From 980b64d4907503cf89f00b2c3fd7f23ae2366808 Mon Sep 17 00:00:00 2001
From: nkdengineer <nkdengineer@outlook.com>
Date: Sun, 22 Feb 2026 20:52:10 +0700
Subject: [PATCH 4/4] add unit test

---
 tests/ui/components/MenuItemTest.tsx | 86 ++++++++++++++++++++++++++++
 1 file changed, 86 insertions(+)
 create mode 100644 tests/ui/components/MenuItemTest.tsx

diff --git a/tests/ui/components/MenuItemTest.tsx b/tests/ui/components/MenuItemTest.tsx
new file mode 100644
index 000000000000..7db09cde04bf
--- /dev/null
+++ b/tests/ui/components/MenuItemTest.tsx
@@ -0,0 +1,86 @@
+import {render, screen} from '@testing-library/react-native';
+import React from 'react';
+import type {SvgProps} from 'react-native-svg';
+import * as Expensicons from '@components/Icon/Expensicons';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import MenuItem from '@components/MenuItem';
+import {translateLocal} from '../../utils/TestHelper';
+
+// Mock useLazyAsset hook
+jest.mock('@hooks/useLazyAsset', () => {
+    const ExpensiconsModule = jest.requireActual<Record<string, React.FC<SvgProps>>>('@components/Icon/Expensicons');
+    return {
+        useMemoizedLazyExpensifyIcons: jest.fn(() => ({
+            NewWindow: ExpensiconsModule.NewWindow,
+        })),
+    };
+});
+
+function Wrapper({children}: {children: React.ReactNode}) {
+    return <LocaleContextProvider>{children}</LocaleContextProvider>;
+}
+
+describe('MenuItem', () => {
+    describe('accessibility label with NewWindow icon', () => {
+        it('appends "Opens in a new tab" to the accessibility label when iconRight is NewWindow', () => {
+            const customLabel = 'Open external link';
+            const opensInNewTabText = translateLocal('common.opensInNewTab');
+            const expectedAccessibilityLabel = `${customLabel}. ${opensInNewTabText}`;
+
+            render(
+                <Wrapper>
+                    <MenuItem
+                        title={customLabel}
+                        icon={Expensicons.Link}
+                        iconRight={Expensicons.NewWindow}
+                        accessibilityLabel={customLabel}
+                        onPress={() => {}}
+                    />
+                </Wrapper>,
+            );
+
+            const menuItem = screen.getByLabelText(expectedAccessibilityLabel);
+            expect(menuItem).toBeOnTheScreen();
+        });
+
+        it('uses default accessibility label (from title) with "Opens in a new tab" when iconRight is NewWindow and no accessibilityLabel is passed', () => {
+            const title = 'Download app';
+            const opensInNewTabText = translateLocal('common.opensInNewTab');
+            const expectedAccessibilityLabel = `${title}. ${opensInNewTabText}`;
+
+            render(
+                <Wrapper>
+                    <MenuItem
+                        title={title}
+                        icon={Expensicons.Download}
+                        iconRight={Expensicons.NewWindow}
+                        onPress={() => {}}
+                    />
+                </Wrapper>,
+            );
+
+            const menuItem = screen.getByLabelText(expectedAccessibilityLabel);
+            expect(menuItem).toBeOnTheScreen();
+        });
+
+        it('does not append "Opens in a new tab" when iconRight is not NewWindow', () => {
+            const customLabel = 'Go to next';
+
+            render(
+                <Wrapper>
+                    <MenuItem
+                        title={customLabel}
+                        icon={Expensicons.Checkmark}
+                        iconRight={Expensicons.Checkmark}
+                        accessibilityLabel={customLabel}
+                        onPress={() => {}}
+                    />
+                </Wrapper>,
+            );
+
+            const menuItem = screen.getByLabelText(customLabel);
+            expect(menuItem).toBeOnTheScreen();
+            expect(screen.queryByLabelText(`${customLabel}. ${translateLocal('common.opensInNewTab')}`)).not.toBeOnTheScreen();
+        });
+    });
+});
