From c4b945256604557fe5ce7dba31f5c8480605e2dd Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 24 May 2026 12:28:42 +0530 Subject: [PATCH 1/8] fix: break VoiceOver grouping on iOS so task checkbox is independently focusable Signed-off-by: krishna2323 --- .../ReportActionItem/TaskPreview.tsx | 58 +++++++++++++------ src/components/ReportActionItem/TaskView.tsx | 48 ++++++++++----- .../index.ios.ts | 8 +++ .../shouldBreakAccessibilityGrouping/index.ts | 7 +++ .../inbox/report/PureReportActionItem.tsx | 3 + 5 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 src/libs/shouldBreakAccessibilityGrouping/index.ios.ts create mode 100644 src/libs/shouldBreakAccessibilityGrouping/index.ts diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 46dfe1fd46fe..dab911a337ec 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -33,6 +33,7 @@ import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {getOriginalMessage} from '@libs/ReportActionsUtils'; import {isCanceledTaskReport, isOpenTaskReport, isReportManager} from '@libs/ReportUtils'; +import shouldBreakAccessibilityGrouping from '@libs/shouldBreakAccessibilityGrouping'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -106,6 +107,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere const iconWrapperStyle = StyleUtils.getTaskPreviewIconWrapper(hasAssignee ? avatarSize : undefined); const shouldShowGreenDotIndicator = isOpenTaskReport(taskContextReport, action) && isReportManager(taskContextReport); + const taskAccessibilityLabel = taskTitleWithoutImage ? `${translate('task.task')}: ${taskTitleWithoutImage}` : translate('task.task'); if (isDeletedParentAction) { return ${translate('parentReportAction.deletedTask')}`} />; } @@ -121,6 +123,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere return ( Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID, undefined, undefined, Navigation.getActiveRoute()))} onPressIn={() => canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} @@ -135,7 +138,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere shouldUseHapticsOnLongPress style={[styles.flexRow, styles.justifyContentBetween, style]} role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('task.task')} + accessibilityLabel={taskAccessibilityLabel} sentryLabel={CONST.SENTRY_LABEL.TASK.PREVIEW_CARD} > @@ -151,26 +154,45 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere completeTask(taskContextReport, parentReport?.hasOutstandingChildTask ?? false, hasOutstandingChildTask, parentReportAction, delegateEmail, taskReportID); } })} - accessibilityLabel={translate('task.task')} + accessibilityLabel={taskAccessibilityLabel} sentryLabel={CONST.SENTRY_LABEL.TASK.PREVIEW_CHECKBOX} /> - {hasAssignee && ( - - - - - - )} - - - + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID, undefined, undefined, Navigation.getActiveRoute()))} + onPressIn={() => canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onLongPress={(event) => + onShowContextMenu(() => { + if (!shouldDisplayContextMenu) { + return; + } + return showContextMenuForReport(event, contextMenuAnchorRef, chatReportID, action, checkIfContextMenuActive, originalReportID); + }) + } + shouldUseHapticsOnLongPress + style={[styles.flex1, styles.flexRow, styles.alignItemsStart]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={taskAccessibilityLabel} + sentryLabel={CONST.SENTRY_LABEL.TASK.PREVIEW_CARD} + > + {hasAssignee && ( + + + + + + )} + + + + {shouldShowGreenDotIndicator && ( diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index e6fa0efd6a8f..594658566569 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -11,6 +11,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import RenderHTML from '@components/RenderHTML'; import {ShowContextMenuActionsContext, ShowContextMenuStateContext} from '@components/ShowContextMenuContext'; @@ -30,6 +31,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import {getDisplayNameForParticipant, getDisplayNamesWithTooltips, isCompletedTaskReport, isOpenTaskReport} from '@libs/ReportUtils'; +import shouldBreakAccessibilityGrouping from '@libs/shouldBreakAccessibilityGrouping'; import StringUtils from '@libs/StringUtils'; import {isActiveTaskEditRoute} from '@libs/TaskUtils'; import {callFunctionIfActionIsAllowed} from '@userActions/Session'; @@ -70,6 +72,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { const taskTitleWithoutPre = StringUtils.removePreCodeBlock(report?.reportName); const titleWithoutImage = Parser.replace(Parser.htmlToMarkdown(taskTitleWithoutPre), {disabledRules: [...CONST.TASK_TITLE_DISABLED_RULES]}); const taskTitle = `${titleWithoutImage}`; + const taskAccessibilityLabel = titleWithoutImage ? `${translate('task.task')}: ${titleWithoutImage}` : translate('task.task'); const assigneeTooltipDetails = getDisplayNamesWithTooltips( getPersonalDetailsForAccountIDs(report?.managerID ? [report?.managerID] : [], personalDetails), @@ -136,6 +139,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { {(hovered) => ( { if (isDisableInteractive) { return; @@ -152,7 +156,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, false, disableState, !isDisableInteractive), true), isDisableInteractive && styles.cursorDefault, ]} - accessibilityLabel={taskTitle || translate('task.task')} + accessibilityLabel={taskAccessibilityLabel} disabled={isDisableInteractive} sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_TITLE} > @@ -162,7 +166,6 @@ function TaskView({report, parentReport, action}: TaskViewProps) { { - // If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page. if (isActiveTaskEditRoute(report?.reportID)) { return; } @@ -177,22 +180,39 @@ function TaskView({report, parentReport, action}: TaskViewProps) { containerSize={24} containerBorderRadius={8} caretSize={16} - accessibilityLabel={taskTitle || translate('task.task')} + accessibilityLabel={taskAccessibilityLabel} disabled={!isTaskActionable} sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_CHECKBOX} /> - - - - {!isDisableInteractive && ( - - + { + if (isDisableInteractive) { + return; + } + if (e?.type === 'click') { + (e.currentTarget as HTMLElement).blur(); + } + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TASK_TITLE.path)); + })} + role={CONST.ROLE.BUTTON} + accessibilityLabel={taskAccessibilityLabel} + disabled={isDisableInteractive} + style={[styles.flexRow, styles.flex1]} + sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_TITLE} + > + + - )} + {!isDisableInteractive && ( + + + + )} + )} diff --git a/src/libs/shouldBreakAccessibilityGrouping/index.ios.ts b/src/libs/shouldBreakAccessibilityGrouping/index.ios.ts new file mode 100644 index 000000000000..90b1083ee8bf --- /dev/null +++ b/src/libs/shouldBreakAccessibilityGrouping/index.ios.ts @@ -0,0 +1,8 @@ +/** + * On iOS, VoiceOver groups all children of an accessible parent into a single + * focus target, preventing individual elements from being focusable. Returning + * true signals that the parent should set accessible={false} to break this grouping. + */ +export default function shouldBreakAccessibilityGrouping(): boolean { + return true; +} diff --git a/src/libs/shouldBreakAccessibilityGrouping/index.ts b/src/libs/shouldBreakAccessibilityGrouping/index.ts new file mode 100644 index 000000000000..ce318988f52c --- /dev/null +++ b/src/libs/shouldBreakAccessibilityGrouping/index.ts @@ -0,0 +1,7 @@ +/** + * On non-iOS platforms, accessible elements don't group children in a way + * that prevents individual focus, so we don't need to break grouping. + */ +export default function shouldBreakAccessibilityGrouping(): boolean { + return false; +} diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index f67cb244e544..bacdef2b2391 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -45,6 +45,7 @@ import { getReportActionMessage, getReportActionText, getWhisperedTo, + isCreatedTaskReportAction, isDeletedParentAction as isDeletedParentActionUtils, isMessageDeleted, isMoneyRequestAction, @@ -62,6 +63,7 @@ import { shouldDisplayThreadReplies as shouldDisplayThreadRepliesUtils, } from '@libs/ReportUtils'; import SelectionScraper from '@libs/SelectionScraper'; +import shouldBreakAccessibilityGrouping from '@libs/shouldBreakAccessibilityGrouping'; import {ReactionListContext} from '@pages/inbox/ReportScreenContext'; import AttachmentModalContext from '@pages/media/AttachmentModalScreen/AttachmentModalContext'; import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors'; @@ -522,6 +524,7 @@ function PureReportActionItem({ )} { if (!hasDraft) { onPress?.(); From 37a3806a51787d2aa5755aec878bb0948871b969 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Mon, 25 May 2026 04:54:30 +0530 Subject: [PATCH 2/8] fix: conditionally render inner pressable only on iOS to avoid regressions on other platforms Signed-off-by: krishna2323 --- .../ReportActionItem/TaskPreview.tsx | 80 +++++++++++-------- src/components/ReportActionItem/TaskView.tsx | 73 ++++++++++------- 2 files changed, 90 insertions(+), 63 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index dab911a337ec..a2d500ffd017 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -158,41 +158,51 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere sentryLabel={CONST.SENTRY_LABEL.TASK.PREVIEW_CHECKBOX} /> - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID, undefined, undefined, Navigation.getActiveRoute()))} - onPressIn={() => canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - onLongPress={(event) => - onShowContextMenu(() => { - if (!shouldDisplayContextMenu) { - return; - } - return showContextMenuForReport(event, contextMenuAnchorRef, chatReportID, action, checkIfContextMenuActive, originalReportID); - }) - } - shouldUseHapticsOnLongPress - style={[styles.flex1, styles.flexRow, styles.alignItemsStart]} - role={CONST.ROLE.BUTTON} - accessibilityLabel={taskAccessibilityLabel} - sentryLabel={CONST.SENTRY_LABEL.TASK.PREVIEW_CARD} - > - {hasAssignee && ( - - - - - - )} - - - - + {shouldBreakAccessibilityGrouping() ? ( + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID, undefined, undefined, Navigation.getActiveRoute()))} + style={[styles.flex1, styles.flexRow, styles.alignItemsStart]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={taskAccessibilityLabel} + sentryLabel={CONST.SENTRY_LABEL.TASK.PREVIEW_CARD} + > + {hasAssignee && ( + + + + + + )} + + + + + ) : ( + <> + {hasAssignee && ( + + + + + + )} + + + + + )} {shouldShowGreenDotIndicator && ( diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 594658566569..4daaf253e5a0 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -184,35 +184,52 @@ function TaskView({report, parentReport, action}: TaskViewProps) { disabled={!isTaskActionable} sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_CHECKBOX} /> - { - if (isDisableInteractive) { - return; - } - if (e?.type === 'click') { - (e.currentTarget as HTMLElement).blur(); - } - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TASK_TITLE.path)); - })} - role={CONST.ROLE.BUTTON} - accessibilityLabel={taskAccessibilityLabel} - disabled={isDisableInteractive} - style={[styles.flexRow, styles.flex1]} - sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_TITLE} - > - - - - {!isDisableInteractive && ( - - + {shouldBreakAccessibilityGrouping() ? ( + { + if (isDisableInteractive) { + return; + } + if (e?.type === 'click') { + (e.currentTarget as HTMLElement).blur(); + } + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TASK_TITLE.path)); + })} + role={CONST.ROLE.BUTTON} + accessibilityLabel={taskAccessibilityLabel} + disabled={isDisableInteractive} + style={[styles.flexRow, styles.flex1]} + sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_TITLE} + > + + + + {!isDisableInteractive && ( + + + + )} + + ) : ( + <> + + - )} - + {!isDisableInteractive && ( + + + + )} + + )} )} From 8e10ec27c8b1674483184892d711eac643765b03 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Mon, 25 May 2026 08:17:52 +0530 Subject: [PATCH 3/8] fix: use local state for checkbox to announce correct checked state after toggle Signed-off-by: krishna2323 --- src/components/ReportActionItem/TaskPreview.tsx | 13 +++++++++++-- src/components/ReportActionItem/TaskView.tsx | 12 ++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index a2d500ffd017..6cf9cfd6e44e 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -1,5 +1,5 @@ import {delegateEmailSelector} from '@selectors/Account'; -import React from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -89,9 +89,17 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere // The reportAction might not contain details regarding the taskReport // Only the direct parent reportAction will contain details about the taskReport // Other linked reportActions will only contain the taskReportID and we will grab the details from there - const isTaskCompleted = !isEmptyObject(taskReport) + const isTaskCompletedFromOnyx = !isEmptyObject(taskReport) ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; + const [prevIsTaskCompletedFromOnyx, setPrevIsTaskCompletedFromOnyx] = useState(isTaskCompletedFromOnyx); + const [isTaskCompleted, setIsTaskCompleted] = useState(isTaskCompletedFromOnyx); + + if (prevIsTaskCompletedFromOnyx !== isTaskCompletedFromOnyx) { + setPrevIsTaskCompletedFromOnyx(isTaskCompletedFromOnyx); + setIsTaskCompleted(isTaskCompletedFromOnyx); + } + const parentReportAction = useParentReportAction(taskContextReport); const taskAssigneeAccountID = getTaskAssigneeAccountID(taskContextReport, parentReportAction) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const parentReport = useParentReport(taskContextReport?.reportID); @@ -148,6 +156,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere isChecked={isTaskCompleted} disabled={!isTaskActionable} onPress={callFunctionIfActionIsAllowed(() => { + setIsTaskCompleted((prev) => !prev); if (isTaskCompleted) { reopenTask(taskContextReport, parentReport, currentUserPersonalDetails.accountID, delegateEmail, taskReportID); } else { diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 4daaf253e5a0..f46f9dbc78bc 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -1,6 +1,6 @@ import {delegateEmailSelector} from '@selectors/Account'; import {hasSeenTourSelector} from '@selectors/Onboarding'; -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {AttachmentContext} from '@components/AttachmentContext'; @@ -82,7 +82,14 @@ function TaskView({report, parentReport, action}: TaskViewProps) { ); const isOpen = isOpenTaskReport(report); - const isCompleted = isCompletedTaskReport(report); + const isCompletedFromOnyx = isCompletedTaskReport(report); + const [prevIsCompletedFromOnyx, setPrevIsCompletedFromOnyx] = useState(isCompletedFromOnyx); + const [isCompleted, setIsCompleted] = useState(isCompletedFromOnyx); + + if (prevIsCompletedFromOnyx !== isCompletedFromOnyx) { + setPrevIsCompletedFromOnyx(isCompletedFromOnyx); + setIsCompleted(isCompletedFromOnyx); + } const isParentReportArchived = useReportIsArchived(parentReport?.reportID); const hasOutstandingChildTask = useHasOutstandingChildTask(report); const isTaskModifiable = canModifyTask(report, currentUserPersonalDetails.accountID, isParentReportArchived); @@ -169,6 +176,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { if (isActiveTaskEditRoute(report?.reportID)) { return; } + setIsCompleted((prev) => !prev); if (isCompleted) { reopenTask(report, parentReport, currentUserPersonalDetails.accountID, delegateEmail); } else { From 2bd995f11bc9c694ff84ed9e8306151c93556c48 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Mon, 25 May 2026 09:12:16 +0530 Subject: [PATCH 4/8] fix: only break accessibility grouping when screen reader is active Signed-off-by: krishna2323 --- src/components/ReportActionItem/TaskPreview.tsx | 7 +++++-- src/components/ReportActionItem/TaskView.tsx | 7 +++++-- src/pages/inbox/report/PureReportActionItem.tsx | 6 +++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 6cf9cfd6e44e..e81466d3536d 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -23,6 +23,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Accessibility from '@libs/Accessibility'; import {callFunctionIfActionIsAllowed} from '@libs/actions/Session'; import {canActionTask, completeTask, getTaskAssigneeAccountID, reopenTask} from '@libs/actions/Task'; import ControlSelection from '@libs/ControlSelection'; @@ -64,6 +65,8 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const theme = useTheme(); + const isScreenReaderActive = Accessibility.useScreenReaderStatus(); + const shouldBreakGrouping = shouldBreakAccessibilityGrouping() && isScreenReaderActive; const {originalReportID, anchor: contextMenuAnchorRef, shouldDisplayContextMenu = true} = useShowContextMenuState(); const {checkIfContextMenuActive, onShowContextMenu} = useShowContextMenuActions(); const originalMessage = getOriginalMessage(action); @@ -131,7 +134,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere return ( Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID, undefined, undefined, Navigation.getActiveRoute()))} onPressIn={() => canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} @@ -167,7 +170,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere sentryLabel={CONST.SENTRY_LABEL.TASK.PREVIEW_CHECKBOX} /> - {shouldBreakAccessibilityGrouping() ? ( + {shouldBreakGrouping ? ( Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID, undefined, undefined, Navigation.getActiveRoute()))} style={[styles.flex1, styles.flexRow, styles.alignItemsStart]} diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index f46f9dbc78bc..8f9c2903f692 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -25,6 +25,7 @@ import useParentReportAction from '@hooks/useParentReportAction'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import Accessibility from '@libs/Accessibility'; import getButtonState from '@libs/getButtonState'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -56,6 +57,8 @@ function TaskView({report, parentReport, action}: TaskViewProps) { const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); const {translate, localeCompare, formatPhoneNumber} = useLocalize(); const styles = useThemeStyles(); + const isScreenReaderActive = Accessibility.useScreenReaderStatus(); + const shouldBreakGrouping = shouldBreakAccessibilityGrouping() && isScreenReaderActive; const StyleUtils = useStyleUtils(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalDetails = usePersonalDetails(); @@ -146,7 +149,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { {(hovered) => ( { if (isDisableInteractive) { return; @@ -192,7 +195,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { disabled={!isTaskActionable} sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_CHECKBOX} /> - {shouldBreakAccessibilityGrouping() ? ( + {shouldBreakGrouping ? ( { if (isDisableInteractive) { diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index bacdef2b2391..4a44d9f84310 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -26,6 +26,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Accessibility from '@libs/Accessibility'; import {cleanUpMoneyRequest} from '@libs/actions/IOU/DeleteMoneyRequest'; import {isSafari} from '@libs/Browser'; import {isChronosOOOListAction} from '@libs/ChronosUtils'; @@ -234,6 +235,9 @@ function PureReportActionItem({ const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID; const [isReportActionActive, setIsReportActionActive] = useState(!!isReportActionLinked); + const isScreenReaderActive = Accessibility.useScreenReaderStatus(); + const shouldBreakGrouping = shouldBreakAccessibilityGrouping() && isScreenReaderActive; + const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID); const shouldRenderViewBasedOnAction = useTableReportViewActionRenderConditionals(action); @@ -524,7 +528,7 @@ function PureReportActionItem({ )} { if (!hasDraft) { onPress?.(); From 661fac796bbad3b76e64d58bef1100c95c93855a Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 28 May 2026 10:02:32 +0530 Subject: [PATCH 5/8] fix: scope local checkbox state to screen reader and remove unreachable click check Signed-off-by: krishna2323 --- .../ReportActionItem/TaskPreview.tsx | 15 +++++++++++---- src/components/ReportActionItem/TaskView.tsx | 18 +++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index af5bae36f12a..74bc554b0647 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -95,14 +95,19 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere const isTaskCompletedFromOnyx = !isEmptyObject(taskReport) ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; + + // Local state provides immediate feedback for VoiceOver after toggling the checkbox, + // since Onyx updates asynchronously and the screen reader would announce the stale state. const [prevIsTaskCompletedFromOnyx, setPrevIsTaskCompletedFromOnyx] = useState(isTaskCompletedFromOnyx); - const [isTaskCompleted, setIsTaskCompleted] = useState(isTaskCompletedFromOnyx); + const [localIsTaskCompleted, setLocalIsTaskCompleted] = useState(isTaskCompletedFromOnyx); if (prevIsTaskCompletedFromOnyx !== isTaskCompletedFromOnyx) { setPrevIsTaskCompletedFromOnyx(isTaskCompletedFromOnyx); - setIsTaskCompleted(isTaskCompletedFromOnyx); + setLocalIsTaskCompleted(isTaskCompletedFromOnyx); } + const isTaskCompleted = shouldBreakGrouping ? localIsTaskCompleted : isTaskCompletedFromOnyx; + const parentReportAction = useParentReportAction(taskContextReport); const taskAssigneeAccountID = getTaskAssigneeAccountID(taskContextReport, parentReportAction) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const parentReport = useParentReport(taskContextReport?.reportID); @@ -159,7 +164,9 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere isChecked={isTaskCompleted} disabled={!isTaskActionable} onPress={callFunctionIfActionIsAllowed(() => { - setIsTaskCompleted((prev) => !prev); + if (shouldBreakGrouping) { + setLocalIsTaskCompleted((prev) => !prev); + } if (isTaskCompleted) { reopenTask(taskContextReport, parentReport, currentUserPersonalDetails.accountID, delegateEmail, taskReportID); } else { @@ -172,7 +179,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere {shouldBreakGrouping ? ( Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID, undefined, undefined, Navigation.getActiveRoute()))} + onPress={() => Navigation.navigate(getReportRouteForCurrentContext({reportID: taskReportID}))} style={[styles.flex1, styles.flexRow, styles.alignItemsStart]} role={CONST.ROLE.BUTTON} accessibilityLabel={taskAccessibilityLabel} diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 8f9c2903f692..b441a94c0c27 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -86,13 +86,18 @@ function TaskView({report, parentReport, action}: TaskViewProps) { const isOpen = isOpenTaskReport(report); const isCompletedFromOnyx = isCompletedTaskReport(report); + + // Local state provides immediate feedback for VoiceOver after toggling the checkbox, + // since Onyx updates asynchronously and the screen reader would announce the stale state. const [prevIsCompletedFromOnyx, setPrevIsCompletedFromOnyx] = useState(isCompletedFromOnyx); - const [isCompleted, setIsCompleted] = useState(isCompletedFromOnyx); + const [localIsCompleted, setLocalIsCompleted] = useState(isCompletedFromOnyx); if (prevIsCompletedFromOnyx !== isCompletedFromOnyx) { setPrevIsCompletedFromOnyx(isCompletedFromOnyx); - setIsCompleted(isCompletedFromOnyx); + setLocalIsCompleted(isCompletedFromOnyx); } + + const isCompleted = shouldBreakGrouping ? localIsCompleted : isCompletedFromOnyx; const isParentReportArchived = useReportIsArchived(parentReport?.reportID); const hasOutstandingChildTask = useHasOutstandingChildTask(report); const isTaskModifiable = canModifyTask(report, currentUserPersonalDetails.accountID, isParentReportArchived); @@ -179,7 +184,9 @@ function TaskView({report, parentReport, action}: TaskViewProps) { if (isActiveTaskEditRoute(report?.reportID)) { return; } - setIsCompleted((prev) => !prev); + if (shouldBreakGrouping) { + setLocalIsCompleted((prev) => !prev); + } if (isCompleted) { reopenTask(report, parentReport, currentUserPersonalDetails.accountID, delegateEmail); } else { @@ -197,13 +204,10 @@ function TaskView({report, parentReport, action}: TaskViewProps) { /> {shouldBreakGrouping ? ( { + onPress={callFunctionIfActionIsAllowed(() => { if (isDisableInteractive) { return; } - if (e?.type === 'click') { - (e.currentTarget as HTMLElement).blur(); - } Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TASK_TITLE.path)); })} role={CONST.ROLE.BUTTON} From 9523a108e8a46e49a6f3533ff9a257e88676aef9 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 28 May 2026 10:37:54 +0530 Subject: [PATCH 6/8] fix: use View with accessibilityActions instead of PressableWithoutFeedback for title area Signed-off-by: krishna2323 --- .../ReportActionItem/TaskPreview.tsx | 24 +++++++++++------- src/components/ReportActionItem/TaskView.tsx | 25 +++++++++---------- .../inbox/report/PureReportActionItem.tsx | 4 +-- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 74bc554b0647..3d0d71c2c5f7 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -66,7 +66,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere const {translate} = useLocalize(); const theme = useTheme(); const isScreenReaderActive = Accessibility.useScreenReaderStatus(); - const shouldBreakGrouping = shouldBreakAccessibilityGrouping() && isScreenReaderActive; + const shouldBreakGrouping = shouldBreakAccessibilityGrouping(); const {originalReportID, anchor: contextMenuAnchorRef, shouldDisplayContextMenu = true} = useShowContextMenuState(); const {checkIfContextMenuActive, onShowContextMenu} = useShowContextMenuActions(); const originalMessage = getOriginalMessage(action); @@ -106,7 +106,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere setLocalIsTaskCompleted(isTaskCompletedFromOnyx); } - const isTaskCompleted = shouldBreakGrouping ? localIsTaskCompleted : isTaskCompletedFromOnyx; + const isTaskCompleted = shouldBreakGrouping && isScreenReaderActive ? localIsTaskCompleted : isTaskCompletedFromOnyx; const parentReportAction = useParentReportAction(taskContextReport); const taskAssigneeAccountID = getTaskAssigneeAccountID(taskContextReport, parentReportAction) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; @@ -164,7 +164,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere isChecked={isTaskCompleted} disabled={!isTaskActionable} onPress={callFunctionIfActionIsAllowed(() => { - if (shouldBreakGrouping) { + if (shouldBreakGrouping && isScreenReaderActive) { setLocalIsTaskCompleted((prev) => !prev); } if (isTaskCompleted) { @@ -178,12 +178,18 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere /> {shouldBreakGrouping ? ( - Navigation.navigate(getReportRouteForCurrentContext({reportID: taskReportID}))} - style={[styles.flex1, styles.flexRow, styles.alignItemsStart]} - role={CONST.ROLE.BUTTON} + { + if (event.nativeEvent.actionName !== 'activate') { + return; + } + Navigation.navigate(getReportRouteForCurrentContext({reportID: taskReportID})); + }} + style={[styles.flex1, styles.flexRow, styles.alignItemsStart]} > {hasAssignee && ( @@ -201,7 +207,7 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere - + ) : ( <> {hasAssignee && ( diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index b441a94c0c27..4d8de76f9b4f 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -11,7 +11,6 @@ import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import RenderHTML from '@components/RenderHTML'; import {ShowContextMenuActionsContext, ShowContextMenuStateContext} from '@components/ShowContextMenuContext'; @@ -58,7 +57,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { const {translate, localeCompare, formatPhoneNumber} = useLocalize(); const styles = useThemeStyles(); const isScreenReaderActive = Accessibility.useScreenReaderStatus(); - const shouldBreakGrouping = shouldBreakAccessibilityGrouping() && isScreenReaderActive; + const shouldBreakGrouping = shouldBreakAccessibilityGrouping(); const StyleUtils = useStyleUtils(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalDetails = usePersonalDetails(); @@ -97,7 +96,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { setLocalIsCompleted(isCompletedFromOnyx); } - const isCompleted = shouldBreakGrouping ? localIsCompleted : isCompletedFromOnyx; + const isCompleted = shouldBreakGrouping && isScreenReaderActive ? localIsCompleted : isCompletedFromOnyx; const isParentReportArchived = useReportIsArchived(parentReport?.reportID); const hasOutstandingChildTask = useHasOutstandingChildTask(report); const isTaskModifiable = canModifyTask(report, currentUserPersonalDetails.accountID, isParentReportArchived); @@ -184,7 +183,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { if (isActiveTaskEditRoute(report?.reportID)) { return; } - if (shouldBreakGrouping) { + if (shouldBreakGrouping && isScreenReaderActive) { setLocalIsCompleted((prev) => !prev); } if (isCompleted) { @@ -203,18 +202,18 @@ function TaskView({report, parentReport, action}: TaskViewProps) { sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_CHECKBOX} /> {shouldBreakGrouping ? ( - { - if (isDisableInteractive) { + { + if (event.nativeEvent.actionName !== 'activate' || isDisableInteractive) { return; } Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TASK_TITLE.path)); - })} - role={CONST.ROLE.BUTTON} - accessibilityLabel={taskAccessibilityLabel} - disabled={isDisableInteractive} + }} style={[styles.flexRow, styles.flex1]} - sentryLabel={CONST.SENTRY_LABEL.TASK.VIEW_TITLE} > @@ -228,7 +227,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { /> )} - + ) : ( <> diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 0e0c8cbf0f41..ef34134b944e 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -26,7 +26,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import Accessibility from '@libs/Accessibility'; import {cleanUpMoneyRequest} from '@libs/actions/IOU/DeleteMoneyRequest'; import {isSafari} from '@libs/Browser'; import {isChronosOOOListAction} from '@libs/ChronosUtils'; @@ -208,8 +207,7 @@ function PureReportActionItem({ const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID; const [isReportActionActive, setIsReportActionActive] = useState(!!isReportActionLinked); - const isScreenReaderActive = Accessibility.useScreenReaderStatus(); - const shouldBreakGrouping = shouldBreakAccessibilityGrouping() && isScreenReaderActive; + const shouldBreakGrouping = shouldBreakAccessibilityGrouping(); const shouldRenderViewBasedOnAction = useTableReportViewActionRenderConditionals(action); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.chatReportID)}`); From 72a67fd5d061005856d96af0fe27a325856c949c Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 28 May 2026 11:01:52 +0530 Subject: [PATCH 7/8] fix: mark task title as disabled for VoiceOver when not interactive Signed-off-by: krishna2323 --- src/components/ReportActionItem/TaskView.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 4d8de76f9b4f..2b2c27ffddb3 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -206,9 +206,10 @@ function TaskView({report, parentReport, action}: TaskViewProps) { accessible accessibilityRole={CONST.ROLE.BUTTON} accessibilityLabel={taskAccessibilityLabel} - accessibilityActions={[{name: 'activate'}]} + accessibilityState={{disabled: isDisableInteractive}} + accessibilityActions={isDisableInteractive ? [] : [{name: 'activate'}]} onAccessibilityAction={(event) => { - if (event.nativeEvent.actionName !== 'activate' || isDisableInteractive) { + if (event.nativeEvent.actionName !== 'activate') { return; } Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TASK_TITLE.path)); From e51b279a58a3898df245a5b8006e19ce11faaeb2 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 29 May 2026 07:26:55 +0530 Subject: [PATCH 8/8] fix: use plain text for task accessibility label and restore removed comment Signed-off-by: krishna2323 --- src/components/ReportActionItem/TaskPreview.tsx | 3 ++- src/components/ReportActionItem/TaskView.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 3d0d71c2c5f7..998b082eb775 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -123,7 +123,8 @@ function TaskPreview({action, chatReportID, currentUserPersonalDetails, isHovere const iconWrapperStyle = StyleUtils.getTaskPreviewIconWrapper(hasAssignee ? avatarSize : undefined); const shouldShowGreenDotIndicator = isOpenTaskReport(taskContextReport, action) && isReportManager(taskContextReport); - const taskAccessibilityLabel = taskTitleWithoutImage ? `${translate('task.task')}: ${taskTitleWithoutImage}` : translate('task.task'); + const taskTitlePlainText = Parser.htmlToText(taskTitle); + const taskAccessibilityLabel = taskTitlePlainText ? `${translate('task.task')}: ${taskTitlePlainText}` : translate('task.task'); if (isDeletedParentAction) { return ${translate('parentReportAction.deletedTask')}`} />; } diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 2b2c27ffddb3..9c7978489ebf 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -74,7 +74,8 @@ function TaskView({report, parentReport, action}: TaskViewProps) { const taskTitleWithoutPre = StringUtils.removePreCodeBlock(report?.reportName); const titleWithoutImage = Parser.replace(Parser.htmlToMarkdown(taskTitleWithoutPre), {disabledRules: [...CONST.TASK_TITLE_DISABLED_RULES]}); const taskTitle = `${titleWithoutImage}`; - const taskAccessibilityLabel = titleWithoutImage ? `${translate('task.task')}: ${titleWithoutImage}` : translate('task.task'); + const taskTitlePlainText = Parser.htmlToText(taskTitleWithoutPre); + const taskAccessibilityLabel = taskTitlePlainText ? `${translate('task.task')}: ${taskTitlePlainText}` : translate('task.task'); const assigneeTooltipDetails = getDisplayNamesWithTooltips( getPersonalDetailsForAccountIDs(report?.managerID ? [report?.managerID] : [], personalDetails), @@ -180,6 +181,7 @@ function TaskView({report, parentReport, action}: TaskViewProps) { { + // If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page. if (isActiveTaskEditRoute(report?.reportID)) { return; }