diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png index 8b6f637184d..02d2343edcc 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-from-accounts-id-page-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png index 0cfdffe0d74..04b134ec15e 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png index 08644ff8375..b59df892ada 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js b/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js index c4281c2b991..f79c09bd254 100644 --- a/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js @@ -11,7 +11,7 @@ export class MobileTransactionEntryPage { async fillField(fieldLocator, content) { await fieldLocator.click(); - await this.page.locator('css=[role=combobox] input').fill(content); + await fieldLocator.fill(content); await this.page.keyboard.press('Enter'); } diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.js b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.js index 44799d7145f..df3eed9c9b5 100644 --- a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.js +++ b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { css } from 'glamor'; @@ -14,6 +14,8 @@ function AccountList({ getItemProps, highlightedIndex, embedded, + style, + itemStyle, groupHeaderStyle, }) { let lastItem = null; @@ -25,6 +27,7 @@ function AccountList({ overflow: 'auto', padding: '5px 0', ...(!embedded && { maxHeight: 175 }), + ...style, }} > {items.map((item, idx) => { @@ -93,6 +96,7 @@ function AccountList({ paddingLeft: 20, borderRadius: embedded ? 4 : 0, }, + itemStyle, ])}`} data-testid={ 'account-item' + @@ -109,17 +113,21 @@ function AccountList({ } export default function AccountAutocomplete({ + accounts, embedded, includeClosedAccounts = true, + accountListStyle, + accountListItemStyle, groupHeaderStyle, closeOnBlur, + inputProps, ...props }) { - let accounts = useCachedAccounts() || []; + const cachedAccounts = useCachedAccounts() || []; //remove closed accounts if needed //then sort by closed, then offbudget - accounts = accounts + accounts = (accounts || cachedAccounts) .filter(item => { return includeClosedAccounts ? item : !item.closed; }) @@ -131,6 +139,8 @@ export default function AccountAutocomplete({ } }); + const [accountFieldFocused, setAccountFieldFocused] = useState(false); + return ( { + setAccountFieldFocused(false); + inputProps?.onBlur?.(e); + }, + onFocus: e => { + setAccountFieldFocused(true); + inputProps?.onFocus?.(e); + }, + }} + focused={accountFieldFocused} renderItems={(items, getItemProps, highlightedIndex) => ( )} diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx index 99041a45872..65ffa1be82e 100644 --- a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx @@ -3,12 +3,13 @@ import React, { Fragment, useMemo, type ReactNode, + useState, } from 'react'; import { css } from 'glamor'; import Split from '../../icons/v0/Split'; -import { theme } from '../../style'; +import { type CSSProperties, theme } from '../../style'; import Text from '../common/Text'; import View from '../common/View'; @@ -33,7 +34,10 @@ export type CategoryListProps = { highlightedIndex: number; embedded: boolean; footer?: ReactNode; - groupHeaderStyle?: object; + style?: CSSProperties; + groupHeaderStyle?: CSSProperties; + itemStyle?: CSSProperties; + splitButtonStyle?: CSSProperties; }; function CategoryList({ items, @@ -41,7 +45,10 @@ function CategoryList({ highlightedIndex, embedded, footer, + style, groupHeaderStyle, + itemStyle, + splitButtonStyle, }: CategoryListProps) { let lastGroup = null; @@ -52,6 +59,7 @@ function CategoryList({ overflow: 'auto', padding: '5px 0', ...(!embedded && { maxHeight: 175 }), + ...style, }} > {items.map((item, idx) => { @@ -98,6 +106,7 @@ function CategoryList({ ':active': { backgroundColor: 'rgba(100, 100, 100, .25)', }, + ...splitButtonStyle, }} data-testid="split-transaction-button" > @@ -139,6 +148,7 @@ function CategoryList({ paddingLeft: 20, borderRadius: embedded ? 4 : 0, }, + itemStyle, ])}`} data-testid={ 'category-item' + @@ -159,14 +169,21 @@ function CategoryList({ type CategoryAutocompleteProps = ComponentProps & { categoryGroups: CategoryGroup[]; showSplitOption?: boolean; - groupHeaderStyle?: object; + categoryListStyle?: CSSProperties; + categoryListItemStyle?: CSSProperties; + groupHeaderStyle?: CSSProperties; + splitButtonStyle?: CSSProperties; }; export default function CategoryAutocomplete({ categoryGroups, showSplitOption, embedded, closeOnBlur, + inputProps, + categoryListStyle, groupHeaderStyle, + categoryListItemStyle, + splitButtonStyle, ...props }: CategoryAutocompleteProps) { let categorySuggestions = useMemo( @@ -184,6 +201,8 @@ export default function CategoryAutocomplete({ [categoryGroups], ); + const [categoryFieldFocused, setCategoryFieldFocused] = useState(false); + return ( { + setCategoryFieldFocused(true); + inputProps?.onFocus?.(e); + }, + onBlur: e => { + setCategoryFieldFocused(false); + inputProps?.onBlur?.(e); + }, + }} + focused={categoryFieldFocused} suggestions={categorySuggestions} renderItems={(items, getItemProps, highlightedIndex) => ( )} {...props} diff --git a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.js b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.js index 16f5aa67ab7..86f83ca1308 100644 --- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.js +++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.js @@ -51,7 +51,10 @@ function PayeeList({ highlightedIndex, embedded, inputValue, + style, + itemStyle, groupHeaderStyle, + createPayeeButtonStyle, footer, }) { const { isNarrowWidth } = useResponsive(); @@ -78,6 +81,7 @@ function PayeeList({ overflow: 'auto', padding: '5px 0', ...(!embedded && { maxHeight: 175 }), + ...style, }} > {createNew && ( @@ -94,6 +98,7 @@ function PayeeList({ ':active': { backgroundColor: 'rgba(100, 100, 100, .25)', }, + ...createPayeeButtonStyle, }} > {item.name} @@ -213,7 +219,12 @@ export default function PayeeAutocomplete({ onUpdate, onSelect, onManagePayees, + payeeListStyle, + payeeListItemStyle, groupHeaderStyle, + createPayeeButtonStyle, + makeTransferButtonStyle, + managePayeesButtonStyle, accounts, payees, ...props @@ -285,11 +296,15 @@ export default function PayeeAutocomplete({ focused={payeeFieldFocused} inputProps={{ ...inputProps, - onBlur: () => { + onBlur: e => { setRawPayee(''); setPayeeFieldFocused(false); + inputProps?.onBlur?.(e); + }, + onFocus: e => { + setPayeeFieldFocused(true); + inputProps?.onFocus?.(e); }, - onFocus: () => setPayeeFieldFocused(true), onChange: setRawPayee, }} onUpdate={(value, inputValue) => @@ -360,13 +375,19 @@ export default function PayeeAutocomplete({ highlightedIndex={highlightedIndex} inputValue={inputValue} embedded={embedded} + style={payeeListStyle} + itemStyle={payeeListItemStyle} + createPayeeButtonStyle={createPayeeButtonStyle} groupHeaderStyle={groupHeaderStyle} footer={ {showMakeTransfer && ( )} {showManagePayees && ( - )} diff --git a/packages/desktop-client/src/components/mobile/MobileAmountInput.js b/packages/desktop-client/src/components/mobile/MobileAmountInput.js index c882b44637b..4d85e974b94 100644 --- a/packages/desktop-client/src/components/mobile/MobileAmountInput.js +++ b/packages/desktop-client/src/components/mobile/MobileAmountInput.js @@ -89,8 +89,8 @@ class AmountInput extends PureComponent { // }); // } - onKeyPress = e => { - if (e.nativeEvent.key === 'Backspace' && this.state.text === '') { + onKeyUp = e => { + if (e.key === 'Backspace' && this.state.text === '') { this.setState({ editing: true }); } }; @@ -151,7 +151,7 @@ class AmountInput extends PureComponent { autoCapitalize="none" onChange={e => this.onChangeText(e.target.value)} onBlur={this.onBlur} - onKeyPress={this.onKeyPress} + onKeyUp={this.onKeyUp} data-testid="amount-input" style={{ flex: 1, textAlign: 'center', position: 'absolute' }} /> diff --git a/packages/desktop-client/src/components/mobile/MobileForms.js b/packages/desktop-client/src/components/mobile/MobileForms.js index 4114b2571fc..76e604df7fb 100644 --- a/packages/desktop-client/src/components/mobile/MobileForms.js +++ b/packages/desktop-client/src/components/mobile/MobileForms.js @@ -64,7 +64,8 @@ export const InputField = forwardRef(function InputField( ); }); -export function TapField({ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function TapField({ value, children, disabled, diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.js b/packages/desktop-client/src/components/transactions/MobileTransaction.js index e5d7b45b352..6d63f2b69d5 100644 --- a/packages/desktop-client/src/components/transactions/MobileTransaction.js +++ b/packages/desktop-client/src/components/transactions/MobileTransaction.js @@ -5,6 +5,7 @@ import React, { useEffect, useState, useRef, + createRef, } from 'react'; import { useSelector } from 'react-redux'; import { useNavigate, useParams, Link } from 'react-router-dom'; @@ -53,6 +54,9 @@ import ArrowsSynchronize from '../../icons/v2/ArrowsSynchronize'; import CheckCircle1 from '../../icons/v2/CheckCircle1'; import SvgPencilWriteAlternate from '../../icons/v2/PencilWriteAlternate'; import { styles, colors, theme } from '../../style'; +import AccountAutocomplete from '../autocomplete/AccountAutocomplete'; +import CategoryAutocomplete from '../autocomplete/CategoryAutocomplete'; +import PayeeAutocomplete from '../autocomplete/PayeeAutocomplete'; import Button from '../common/Button'; import Text from '../common/Text'; import TextOneLine from '../common/TextOneLine'; @@ -60,7 +64,6 @@ import View from '../common/View'; import { FocusableAmountInput } from '../mobile/MobileAmountInput'; import { FieldLabel, - TapField, InputField, BooleanField, EDITING_PADDING, @@ -188,6 +191,11 @@ class TransactionEditInner extends PureComponent { transactions: props.transactions, editingChild: null, }; + + this.amountInputRef = createRef(); + this.payeeInputRef = createRef(); + this.categoryInputRef = createRef(); + this.accountInputRef = createRef(); } serializeTransactions = memoizeOne(transactions => { @@ -197,8 +205,8 @@ class TransactionEditInner extends PureComponent { }); componentDidMount() { - if (this.props.adding) { - this.amount.focus(); + if (this.props.isAdding) { + this.amountInputRef.current?.focus(); } } @@ -237,7 +245,7 @@ class TransactionEditInner extends PureComponent { transactions = await this.onEdit(transaction, name, value); } - if (this.props.adding) { + if (this.props.isAdding) { transactions = realizeTempTransactions(transactions); } @@ -306,14 +314,24 @@ class TransactionEditInner extends PureComponent { }; render() { - const { adding, categories, accounts, payees, renderChildEdit, navigate } = - this.props; + const { + isAdding, + currentAccountId, + categories, + categoryGroups, + accounts, + payees, + renderChildEdit, + navigate, + dateFormat, + } = this.props; const { editingChild } = this.state; const transactions = this.serializeTransactions( this.state.transactions || [], ); const [transaction, ..._childTransactions] = transactions; - const { payee: payeeId, category, account: accountId } = transaction; + const { payee: payeeId, category: categoryId } = transaction; + const accountId = currentAccountId || transaction?.account; // Child transactions should always default to the signage // of the parent transaction @@ -332,13 +350,33 @@ class TransactionEditInner extends PureComponent { transferAcct, ); - const transactionDate = parseDate( - transaction.date, - this.props.dateFormat, - new Date(), - ); + const transactionDate = parseDate(transaction.date, dateFormat, new Date()); const dateDefaultValue = monthUtils.dayFromDate(transactionDate); + const autocompletePaddingStyle = { + padding: '7px 7px', + }; + + const isOffBudget = account && account?.offbudget; + const isBudgetTransfer = transferAcct && !transferAcct?.offbudget; + const isParent = transaction?.is_parent; + const isPreview = isPreviewId(transaction?.id); + + const orderedInputRef = [ + this.payeeInputRef, + this.categoryInputRef, + this.accountInputRef, + ]; + + function focusNextInputOrBlur() { + const nextRef = orderedInputRef.find(r => !r.current?.value); + if (nextRef) { + nextRef?.current?.focus(); + } else { + orderedInputRef.forEach(r => r.current?.blur()); + } + } + return ( // {payeeId == null - ? adding + ? isAdding ? 'New Transaction' : 'Transaction' : descriptionPretty} @@ -453,7 +491,7 @@ class TransactionEditInner extends PureComponent { style={{ marginBottom: 0, paddingLeft: 0 }} /> (this.amount = el)} + ref={this.amountInputRef} value={transaction.amount} zeroIsNegative={true} onBlur={value => @@ -477,60 +515,151 @@ class TransactionEditInner extends PureComponent { - this.onClick(transaction.id, 'payee')} data-testid="payee-field" + /> */} + { + this.onEdit(transaction, 'payee', payeeId); + focusNextInputOrBlur(); + }} + // onManagePayees={() => onManagePayees(payeeId)} + isCreatable + menuPortalTarget={undefined} + groupHeaderStyle={autocompletePaddingStyle} + payeeListItemStyle={autocompletePaddingStyle} + createPayeeButtonStyle={autocompletePaddingStyle} + makeTransferButtonStyle={autocompletePaddingStyle} + managePayeesButtonStyle={autocompletePaddingStyle} /> - {!transaction.is_parent ? ( - - // Split - // - // } - onClick={() => this.onClick(transaction.id, 'category')} + {isBudgetTransfer || isOffBudget || isPreview ? ( + - ) : ( + ) : isParent ? ( Split transaction editing is not supported on mobile at this time. + ) : ( + // + // // Split + // // + // // } + // onClick={() => this.onClick(transaction.id, 'category')} + // data-testid="category-field" + // /> + { + this.onEdit(transaction, 'category', categoryId); + focusNextInputOrBlur(); + }} + menuPortalTarget={undefined} + /> )} - this.onClick(transaction.id, 'account')} data-testid="account-field" + /> */} + { + this.onEdit(transaction, 'account', payeeId); + focusNextInputOrBlur(); + }} + menuPortalTarget={undefined} + accountListItemStyle={autocompletePaddingStyle} /> @@ -546,17 +675,14 @@ class TransactionEditInner extends PureComponent { this.onEdit( transaction, 'date', - formatDate(parseISO(value), this.props.dateFormat), + formatDate(parseISO(value), dateFormat), ) } onChange={e => this.onQueueChange( transaction, 'date', - formatDate( - parseISO(e.target.value), - this.props.dateFormat, - ), + formatDate(parseISO(e.target.value), dateFormat), ) } /> @@ -585,7 +711,7 @@ class TransactionEditInner extends PureComponent { /> - {!adding && ( + {!isAdding && (