Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions src/pages/EditReportFieldDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,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 */
Expand Down Expand Up @@ -62,18 +62,21 @@ function EditReportFieldDropdown({

const validFieldOptions = fieldOptions?.filter((option) => !!option)?.sort(localeCompare);

const [draftValue, setDraftValue] = useState<string>();
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 optionSections = 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB: the comment above still says the live value drives the checkmark, it's the staged value now. Can we update it so the two agree?

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.
Expand Down Expand Up @@ -109,10 +112,13 @@ function EditReportFieldDropdown({
titleNumberOfLines={shouldUseCompactRows ? 2 : undefined}
shouldShowTextInput={shouldShowTextInput}
textInputOptions={textInputOptions}
// Re-selecting the value the field already holds submits that same value rather than an empty string.
// Both call sites compare the submitted value against the stored one and skip the save themselves, so
// signalling "no change" this way is no longer needed and leaves '' free to mean "cleared".
onSelectRow={(option) => onSubmit({[fieldKey]: option?.text ?? ''})}
onSelectRow={(option) => setDraftValue(option?.text ?? '')}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Toggle a reselected option to the empty draft

When the user taps the currently checked option—or selects a new option and taps it again—this always stages option.text, so the checkmark cannot be removed and onSubmit can never receive ''. This makes clearing an optional list report field impossible through this picker; compare the selected option with currentValue and stage an empty string when they match.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot Check this comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should change this. On main, tapping the selected list value no longer clears the field. Restore: Show up to three report fields per row and edit them inline made that change on purpose. It replaced the old fieldValue === option.text ? '' toggle with submitting the tapped value.

This PR only moves the save behind a Save button for WCAG 3.2.2. It shouldn't bring back toggle-to-clear. Tapping the selected row now picks the same value, so Save stays disabled. The test in EditReportFieldDropdownTest.tsx covers this.

If you want toggle-to-clear back, let me know and I'll add it.


view run

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the existing tests for the staged submission flow

The existing cases in tests/ui/EditReportFieldDropdownTest.tsx (lines 104–129) press a row and immediately assert that onSubmit was called. Since this callback now only updates local state, both tests fail whenever the UI suite runs. Update them to assert that row selection does not submit, press the new Save button, and then verify the submitted value.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot Check this comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already handled in e5fdb99. The EditReportFieldDropdownTest.tsx tests now check three things:

  • Picking a row doesn't submit.
  • Pressing Save submits the picked value.
  • Picking the stored value again leaves Save disabled.

That commit also fixed SelectionListWithSections so it renders the Save button. No further change needed.


view run

confirmButtonOptions={{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also the list in the inline report field dropdown from #104124 (ReportFieldInlineInput), so the change hits that dropdown too. Picking a value there stops saving, and the Save button goes into a popover sized with hasButton: false, so a short list gets squeezed to about one visible row. If we keep this PR, the Save gate should only apply to DynamicEditReportFieldPage, the way #101686 kept it to isEditingTask.

showButton: true,
text: translate('common.save'),
onConfirm: () => onSubmit({[fieldKey]: currentValue}),
isDisabled: draftValue === undefined || draftValue === fieldValue,
}}
initiallyFocusedItemKey={initialFieldValue}
shouldUpdateFocusedIndex
/>
Expand Down
27 changes: 19 additions & 8 deletions tests/ui/EditReportFieldDropdownTest.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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', () => ({
Expand All @@ -28,7 +30,7 @@ const fieldOptions = ['Alpha', 'Bravo', 'Charlie'];

const renderDropdown = async (props: Partial<React.ComponentProps<typeof EditReportFieldDropdown>> = {}) => {
const rendered = render(
<ComposeProviders components={[OnyxListItemProvider]}>
<ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
<EditReportFieldDropdown
fieldKey={fieldKey}
fieldValue="Alpha"
Expand Down Expand Up @@ -101,7 +103,7 @@ describe('EditReportFieldDropdown', () => {
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});
Expand All @@ -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();
});
});
Loading