From 09dac5055c53ce7a53e02db45c44839775b2eeb8 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Mon, 21 Sep 2026 05:01:54 +0000 Subject: [PATCH 1/3] Gate report field list selection behind a Save button Co-authored-by: truph01 --- src/pages/EditReportFieldDropdown.tsx | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/pages/EditReportFieldDropdown.tsx b/src/pages/EditReportFieldDropdown.tsx index 64af2b99cf86..0125abfedb79 100644 --- a/src/pages/EditReportFieldDropdown.tsx +++ b/src/pages/EditReportFieldDropdown.tsx @@ -11,7 +11,7 @@ import {getReportFieldOptionsSection} from '@libs/ReportFieldOptionsListUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import React from 'react'; +import React, {useState} from 'react'; type EditReportFieldDropdownPageProps = { /** Value of the policy report field */ @@ -35,18 +35,24 @@ function EditReportFieldDropdown({onSubmit, fieldKey, fieldValue, fieldOptions}: const validFieldOptions = fieldOptions?.filter((option) => !!option)?.sort(localeCompare); + // Tapping a row used to call `onSubmit` straight away, which closed the page on input (WCAG 3.2.2 On Input). + // The tap now only stages the value, and `onSubmit` runs from an explicit Save. + // An empty string is a meaningful staged value (it clears the field), so `undefined` means "nothing staged yet". + const [draftValue, setDraftValue] = useState(); + const currentValue = draftValue ?? fieldValue; + // Freeze the value selected when the picker opened so it drives the pinned "Selected" section for the whole open/focus cycle. // The live value still drives the checkmark, so tapping a row marks it without reordering the list. The reorder happens only on reopen. const initialFieldValue = useInitialSelection(fieldValue, {resetOnFocus: true}); const sections = getReportFieldOptionsSection({ searchValue: debouncedSearchValue, - // Live value drives the checkmark, so tapping a row marks it immediately. + // Staged value drives the checkmark, so tapping a row marks it immediately. selectedOptions: [ { - keyForList: fieldValue, - searchText: fieldValue, - text: fieldValue, + keyForList: currentValue, + searchText: currentValue, + text: currentValue, }, ], // Frozen value drives the pinned section, so the list doesn't reorder while selecting. @@ -71,7 +77,14 @@ function EditReportFieldDropdown({onSubmit, fieldKey, fieldValue, fieldOptions}: ListItem={SingleSelectListItem} shouldShowTextInput textInputOptions={textInputOptions} - onSelectRow={(option) => onSubmit({[fieldKey]: !option?.text || fieldValue === option.text ? '' : option.text})} + // Tapping the already-selected row clears the field, matching the previous behavior of this picker. + onSelectRow={(option) => setDraftValue(!option?.text || currentValue === option.text ? '' : option.text)} + confirmButtonOptions={{ + showButton: true, + text: translate('common.save'), + onConfirm: () => onSubmit({[fieldKey]: currentValue}), + isDisabled: draftValue === undefined || draftValue === fieldValue, + }} initiallyFocusedItemKey={initialFieldValue} shouldUpdateFocusedIndex /> From e5fdb99ca9cc56827c2c811831d6002f4488b32b Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Thu, 22 Oct 2026 11:28:50 +0000 Subject: [PATCH 2/3] Fix: render the Save button in sectioned lists and update dropdown tests for Save Co-authored-by: truph01 --- .../BaseSelectionListWithSections.tsx | 1 + tests/ui/EditReportFieldDropdownTest.tsx | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx index e43ca335501f..ad5adcdd499c 100644 --- a/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx +++ b/src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx @@ -342,6 +342,7 @@ function BaseSelectionListWithSectionsImpl({ const footer = ( footerContent={footerContent} + confirmButtonOptions={confirmButtonOptions} addBottomSafeAreaPadding={addBottomSafeAreaPadding} /> ); diff --git a/tests/ui/EditReportFieldDropdownTest.tsx b/tests/ui/EditReportFieldDropdownTest.tsx index 9bc104cc7955..e0aab5f35cc7 100644 --- a/tests/ui/EditReportFieldDropdownTest.tsx +++ b/tests/ui/EditReportFieldDropdownTest.tsx @@ -1,6 +1,7 @@ import {act, fireEvent, render, screen} from '@testing-library/react-native'; import ComposeProviders from '@components/ComposeProviders'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import EditReportFieldDropdown from '@pages/EditReportFieldDropdown'; @@ -12,6 +13,7 @@ import type * as NativeNavigation from '@react-navigation/native'; import React from 'react'; import Onyx from 'react-native-onyx'; +import {translateLocal} from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; jest.mock('@react-navigation/native', () => ({ @@ -28,7 +30,7 @@ const fieldOptions = ['Alpha', 'Bravo', 'Charlie']; const renderDropdown = async (props: Partial> = {}) => { const rendered = render( - + { expect(screen.getAllByTestId('base-list-item-Bravo')).toHaveLength(1); }); - it('submits the option that was picked', async () => { + it('does not submit when an option is picked, only when Save is pressed', async () => { // Given a dropdown whose field currently holds "Alpha" const onSubmit = jest.fn(); await renderDropdown({onSubmit}); @@ -110,22 +112,31 @@ describe('EditReportFieldDropdown', () => { fireEvent.press(screen.getByTestId('base-list-item-Charlie')); await waitForBatchedUpdatesWithAct(); - // Then it is submitted under the field's own key, which is what both the editor page and the inline field + // Then nothing is submitted yet, because closing the page as soon as a row is tapped changes context on input (WCAG 3.2.2) + expect(onSubmit).not.toHaveBeenCalled(); + + // When Save is pressed + fireEvent.press(screen.getByText(translateLocal('common.save'))); + await waitForBatchedUpdatesWithAct(); + + // Then the picked option is submitted under the field's own key, which is what both the editor page and the inline field // rely on to know which field the value belongs to expect(onSubmit).toHaveBeenCalledWith({[fieldKey]: 'Charlie'}); }); - it('submits the current value rather than an empty string when the option already selected is picked again', async () => { + it('does not submit when the option already selected is picked again and Save is pressed', async () => { // Given a dropdown whose field currently holds "Alpha" const onSubmit = jest.fn(); await renderDropdown({onSubmit}); - // When that same option is pressed again + // When that same option is pressed again and then Save is pressed fireEvent.press(screen.getByTestId('base-list-item-Alpha')); await waitForBatchedUpdatesWithAct(); + fireEvent.press(screen.getByText(translateLocal('common.save'))); + await waitForBatchedUpdatesWithAct(); - // Then the current value is submitted, not an empty string. Callers skip a no-op save by comparing this - // against the stored value, so an empty string would be read as the user having cleared the field - expect(onSubmit).toHaveBeenCalledWith({[fieldKey]: 'Alpha'}); + // Then nothing is submitted. Re-picking the stored value stages that same value rather than an empty string, + // so Save stays disabled instead of being read as the user having cleared the field + expect(onSubmit).not.toHaveBeenCalled(); }); }); From d0766968d5ec3c5872fac8631063dbe584e871f7 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Thu, 22 Oct 2026 11:39:53 +0000 Subject: [PATCH 3/3] Remove comments from EditReportFieldDropdown per review Co-authored-by: truph01 --- src/pages/EditReportFieldDropdown.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pages/EditReportFieldDropdown.tsx b/src/pages/EditReportFieldDropdown.tsx index 870d1c39a5b6..37af94514f90 100644 --- a/src/pages/EditReportFieldDropdown.tsx +++ b/src/pages/EditReportFieldDropdown.tsx @@ -62,9 +62,6 @@ function EditReportFieldDropdown({ const validFieldOptions = fieldOptions?.filter((option) => !!option)?.sort(localeCompare); - // Tapping a row used to call `onSubmit` straight away, which closed the page on input (WCAG 3.2.2 On Input). - // The tap now only stages the value, and `onSubmit` runs from an explicit Save. - // An empty string is a meaningful staged value (it clears the field), so `undefined` means "nothing staged yet". const [draftValue, setDraftValue] = useState(); const currentValue = draftValue ?? fieldValue; @@ -115,8 +112,6 @@ function EditReportFieldDropdown({ titleNumberOfLines={shouldUseCompactRows ? 2 : undefined} shouldShowTextInput={shouldShowTextInput} textInputOptions={textInputOptions} - // Re-selecting the value the field already holds stages that same value rather than an empty string, - // which leaves '' free to mean "cleared". Staging the stored value keeps Save disabled. onSelectRow={(option) => setDraftValue(option?.text ?? '')} confirmButtonOptions={{ showButton: true,