diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 5872fadb108a..a07c0e14f1a9 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -571,7 +571,7 @@ function MenuItem({ rightIconWrapperStyle, titleAccessibilityRole, }: MenuItemProps) { - const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'FallbackAvatar', 'DotIndicator', 'Checkmark']); + const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'FallbackAvatar', 'DotIndicator', 'Checkmark', 'NewWindow']); const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -581,7 +581,6 @@ function MenuItem({ const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const popoverAnchor = useRef(null); const deviceHasHoverSupport = hasHoverSupport(); - 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; @@ -722,6 +721,12 @@ function MenuItem({ const isIDPassed = !!iconReportID || !!iconAccountID || iconAccountID === CONST.DEFAULT_NUMBER_ID; + const isNewWindowIcon = iconRight === icons.NewWindow; + let enhancedAccessibilityLabel = accessibilityLabel ?? defaultAccessibilityLabel; + if (isNewWindowIcon) { + enhancedAccessibilityLabel = `${enhancedAccessibilityLabel}. ${translate('common.opensInNewTab')}`; + } + return ( = { exchangeRate: 'Wechselkurs', reimbursableTotal: 'Erstattungsfähiger Gesamtbetrag', nonReimbursableTotal: 'Nicht erstattungsfähige Gesamtsumme', + opensInNewTab: 'Wird in einem neuen Tab geöffnet', locked: 'Gesperrt', month: 'Monat', week: 'Woche', diff --git a/src/languages/en.ts b/src/languages/en.ts index d5c0aeb138f6..216b0a7d0038 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -550,6 +550,7 @@ const translations = { exchangeRate: 'Exchange rate', reimbursableTotal: 'Reimbursable total', nonReimbursableTotal: 'Non-reimbursable total', + opensInNewTab: 'Opens in a new tab', locked: 'Locked', month: 'Month', week: 'Week', diff --git a/src/languages/es.ts b/src/languages/es.ts index dede4ccad19e..907ddd3d618a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -398,6 +398,7 @@ const translations: TranslationDeepObject = { exchangeRate: 'Tipo de cambio', reimbursableTotal: 'Total reembolsable', nonReimbursableTotal: 'Total no reembolsable', + opensInNewTab: 'Se abre en una nueva pestaña', locked: 'Bloqueado', month: 'Monat', week: 'Semana', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 624014061be1..fae8a8b94916 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -531,6 +531,7 @@ const translations: TranslationDeepObject = { exchangeRate: 'Taux de change', reimbursableTotal: 'Total remboursable', nonReimbursableTotal: 'Total non remboursable', + opensInNewTab: 'S’ouvre dans un nouvel onglet', locked: 'Verrouillé', month: 'Mois', week: 'Semaine', diff --git a/src/languages/it.ts b/src/languages/it.ts index 16f35a7e1ba3..6d0ed3b18c2b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -531,6 +531,7 @@ const translations: TranslationDeepObject = { exchangeRate: 'Tasso di cambio', reimbursableTotal: 'Totale rimborsabile', nonReimbursableTotal: 'Totale non rimborsabile', + opensInNewTab: 'Si apre in una nuova scheda', locked: 'Bloccato', month: 'Mese', week: 'Settimana', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index aa873fba0bb7..4411c4690233 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -530,6 +530,7 @@ const translations: TranslationDeepObject = { exchangeRate: '為替レート', reimbursableTotal: '精算対象合計', nonReimbursableTotal: '未払い対象外の合計', + opensInNewTab: '新しいタブで開きます', locked: 'ロックされています', month: '月', week: '週', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d064fdd6e68d..df9ba5fd6cdb 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -526,6 +526,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Actie vereist', duplicate: 'Dupliceren', duplicated: 'Gedupliceerd', + opensInNewTab: 'Opent in een nieuw tabblad', locked: 'Vergrendeld', duplicateExpense: 'Dubbele uitgave', exchangeRate: 'Wisselkoers', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 26711f3d65a5..64e586442293 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -526,6 +526,7 @@ const translations: TranslationDeepObject = { actionRequired: 'Wymagane działanie', duplicate: 'Duplikuj', duplicated: 'Zduplikowano', + opensInNewTab: 'Otwiera się w nowej karcie', locked: 'Zablokowane', duplicateExpense: 'Zduplikowany wydatek', exchangeRate: 'Kurs wymiany', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index fa25aaeefb9e..4e2bb99eaa93 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -529,6 +529,7 @@ const translations: TranslationDeepObject = { exchangeRate: 'Taxa de câmbio', reimbursableTotal: 'Total reembolsável', nonReimbursableTotal: 'Total não reembolsável', + opensInNewTab: 'Abre em uma nova aba', locked: 'Bloqueado', month: 'Mês', week: 'Semana', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d9b83ec742c8..87a6cc473907 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -526,6 +526,7 @@ const translations: TranslationDeepObject = { exchangeRate: '汇率', reimbursableTotal: '可报销总额', nonReimbursableTotal: '不可报销总额', + opensInNewTab: '在新标签页中打开', locked: '已锁定', month: '月', week: '周', 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>>('@components/Icon/Expensicons'); + return { + useMemoizedLazyExpensifyIcons: jest.fn(() => ({ + NewWindow: ExpensiconsModule.NewWindow, + })), + }; +}); + +function Wrapper({children}: {children: React.ReactNode}) { + return {children}; +} + +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( + + {}} + /> + , + ); + + 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( + + {}} + /> + , + ); + + 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( + + {}} + /> + , + ); + + const menuItem = screen.getByLabelText(customLabel); + expect(menuItem).toBeOnTheScreen(); + expect(screen.queryByLabelText(`${customLabel}. ${translateLocal('common.opensInNewTab')}`)).not.toBeOnTheScreen(); + }); + }); +});