From e09afce5240962666a531fa02bb7110e2f779b4c Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Sat, 22 Aug 2026 04:04:41 +0000 Subject: [PATCH 1/3] Gate NetSuite invoice-item preference selection behind a Save button (a11y On Input, PR 13) Co-authored-by: truph01 --- ...etSuiteInvoiceItemPreferenceSelectPage.tsx | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx index e8a057a927f0..6d2d4760464f 100644 --- a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx @@ -26,7 +26,7 @@ import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {ValueOf} from 'type-fest'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; type MenuListItem = ListItem & { @@ -46,40 +46,51 @@ function DynamicNetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConn const selectedValue = Object.values(CONST.NETSUITE_INVOICE_ITEM_PREFERENCE).find((value) => value === config?.invoiceItemPreference) ?? CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE; + const [draftPreference, setDraftPreference] = useState>(); + const currentPreference = draftPreference ?? selectedValue; + const options: MenuListItem[] = useMemo( () => Object.values(CONST.NETSUITE_INVOICE_ITEM_PREFERENCE).map((postingPreference) => ({ value: postingPreference, text: translate(`workspace.netsuite.invoiceItem.values.${postingPreference}.label`), keyForList: postingPreference, - isSelected: selectedValue === postingPreference, + isSelected: currentPreference === postingPreference, })), - [selectedValue, translate], + [currentPreference, translate], ); const goBack = useCallback(() => { Navigation.goBack(backPath); }, [backPath]); - const selectInvoicePreference = useCallback( - (row: MenuListItem) => { - if (row.value !== config?.invoiceItemPreference && policyID) { - updateNetSuiteInvoiceItemPreference(policyID, row.value, config?.invoiceItemPreference); - } - if (row.value === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE) { - goBack(); - } - }, - [config?.invoiceItemPreference, policyID, goBack], + const savePreference = useCallback(() => { + if (currentPreference !== config?.invoiceItemPreference && policyID) { + updateNetSuiteInvoiceItemPreference(policyID, currentPreference, config?.invoiceItemPreference); + } + // Selecting CREATE completes the flow, so we return to the previous screen. SELECT reveals the invoice-item sub-menu, so we stay. + if (currentPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE) { + goBack(); + } + }, [currentPreference, config?.invoiceItemPreference, policyID, goBack]); + + const confirmButtonOptions = useMemo( + () => ({ + showButton: true, + text: translate('common.save'), + onConfirm: savePreference, + isDisabled: currentPreference === selectedValue, + }), + [savePreference, translate, currentPreference, selectedValue], ); - // Update focused index when selectedValue changes (after an error reverts the selection) + // Update focused index when the current preference changes (after an error reverts the selection) useEffect(() => { const selectedIndex = options.findIndex((option) => option.isSelected); if (selectedIndex !== -1 && selectionListRef.current) { selectionListRef.current?.updateFocusedIndex(selectedIndex); } - }, [selectedValue, options]); + }, [currentPreference, options]); return ( { - selectInvoicePreference(selection as MenuListItem); + setDraftPreference((selection as MenuListItem).value); }} ListItem={SingleSelectListItem} + confirmButtonOptions={confirmButtonOptions} showScrollIndicator shouldUpdateFocusedIndex initiallyFocusedItemKey={options.find((mode) => mode.isSelected)?.keyForList} style={{containerStyle: [styles.pb0]}} + addBottomSafeAreaPadding /> {config?.invoiceItemPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT && ( From 712e8f9d2c975a037524732917d087861ba649ca Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Thu, 10 Sep 2026 03:53:33 +0000 Subject: [PATCH 2/3] Pin the Save button to the bottom of the invoice item preference page ConnectionLayout renders a plain View when shouldUseScrollView is false, and the page passed no contentContainerStyle, so that View shrink-wrapped to its content. The footer's marginTop:auto had no surplus height to absorb, leaving Save directly under the options. Pass contentContainerStyle={[styles.flex1]} to match the sibling pages, and move the invoice-item sub-menu into listFooterContent so it renders under the options instead of competing with the list for flex space. Co-authored-by: truph01 --- ...etSuiteInvoiceItemPreferenceSelectPage.tsx | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx index 6d2d4760464f..f8ed0b355274 100644 --- a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx @@ -27,7 +27,6 @@ import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {ValueOf} from 'type-fest'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; type MenuListItem = ListItem & { value: ValueOf; @@ -84,6 +83,33 @@ function DynamicNetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConn [savePreference, translate, currentPreference, selectedValue], ); + // Rendered as the list footer so the invoice-item sub-menu sits directly under the options and the Save button stays pinned to the bottom of the screen. + const invoiceItemFooterContent = useMemo(() => { + if (config?.invoiceItemPreference !== CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT) { + return null; + } + return ( + + { + if (!policyID) { + return; + } + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT.path)); + }} + brickRoadIndicator={areSettingsInErrorFields([CONST.NETSUITE_CONFIG.INVOICE_ITEM], config?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + + ); + }, [config?.invoiceItemPreference, config?.pendingFields, config?.errorFields, policyID, selectedItem, translate]); + // Update focused index when the current preference changes (after an error reverts the selection) useEffect(() => { const selectedIndex = options.findIndex((option) => option.isSelected); @@ -103,6 +129,7 @@ function DynamicNetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConn displayName="DynamicNetSuiteInvoiceItemPreferenceSelectPage" policyID={policyID} connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + contentContainerStyle={[styles.flex1]} shouldUseScrollView={false} > mode.isSelected)?.keyForList} @@ -128,28 +156,6 @@ function DynamicNetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConn addBottomSafeAreaPadding /> - {config?.invoiceItemPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT && ( - - - { - if (!policyID) { - return; - } - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT.path)); - }} - brickRoadIndicator={areSettingsInErrorFields([CONST.NETSUITE_CONFIG.INVOICE_ITEM], config?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - /> - - - )} ); } From 7a79cd803c547e41424ae93d10f5bc93a17261d1 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Thu, 10 Sep 2026 06:57:09 +0000 Subject: [PATCH 3/3] Move the Save button outside OfflineWithFeedback so the error renders under the list Co-authored-by: truph01 --- ...etSuiteInvoiceItemPreferenceSelectPage.tsx | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx index f8ed0b355274..1b545ad163ce 100644 --- a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx @@ -1,4 +1,6 @@ +import Button from '@components/ButtonComposed'; import ConnectionLayout from '@components/ConnectionLayout'; +import FixedFooter from '@components/FixedFooter'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import SelectionList from '@components/SelectionList'; @@ -73,16 +75,6 @@ function DynamicNetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConn } }, [currentPreference, config?.invoiceItemPreference, policyID, goBack]); - const confirmButtonOptions = useMemo( - () => ({ - showButton: true, - text: translate('common.save'), - onConfirm: savePreference, - isDisabled: currentPreference === selectedValue, - }), - [savePreference, translate, currentPreference, selectedValue], - ); - // Rendered as the list footer so the invoice-item sub-menu sits directly under the options and the Save button stays pinned to the bottom of the screen. const invoiceItemFooterContent = useMemo(() => { if (config?.invoiceItemPreference !== CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT) { @@ -147,15 +139,27 @@ function DynamicNetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConn setDraftPreference((selection as MenuListItem).value); }} ListItem={SingleSelectListItem} - confirmButtonOptions={confirmButtonOptions} listFooterContent={invoiceItemFooterContent} showScrollIndicator shouldUpdateFocusedIndex initiallyFocusedItemKey={options.find((mode) => mode.isSelected)?.keyForList} style={{containerStyle: [styles.pb0]}} - addBottomSafeAreaPadding /> + {/* The Save button sits outside OfflineWithFeedback so its error row renders directly under the list rather than under the button. */} + + + ); }