FIX: Accessibility for Transaction rows

This commit is contained in:
Marcos Rodriguez Velez 2024-07-04 15:38:50 -04:00
parent aeb4db00eb
commit 61ee3dfaba
No known key found for this signature in database
GPG key ID: 6030B2F48CCE86D7
4 changed files with 100 additions and 83 deletions

View file

@ -1,5 +1,5 @@
import React, { Ref, useCallback, useMemo } from 'react';
import { Platform, Pressable, TouchableOpacity, View } from 'react-native';
import { Platform, Pressable, TouchableOpacity } from 'react-native';
import {
ContextMenuView,
RenderItem,
@ -10,6 +10,7 @@ import {
} from 'react-native-ios-context-menu';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
import { ToolTipMenuProps, Action } from './types';
import { useSettings } from '../hooks/context/useSettings';
const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
const {
@ -27,6 +28,8 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
...restProps
} = props;
const { language } = useSettings();
const mapMenuItemForContextMenuView = useCallback((action: Action) => {
if (!action.id) return null;
return {
@ -99,6 +102,11 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
return (
<ContextMenuView
lazyPreview
accessibilityLabel={props.accessibilityLabel}
accessibilityHint={props.accessibilityHint}
accessibilityRole={props.accessibilityRole}
accessibilityState={props.accessibilityState}
accessibilityLanguage={language}
shouldEnableAggressiveCleanup
internalCleanupMode="automatic"
onPressMenuItem={handlePressMenuItemForContextMenuView}
@ -133,23 +141,26 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
const renderMenuView = () => {
console.debug('ToolTipMenu.tsx rendering: renderMenuView');
return (
<View>
<MenuView
title={title}
isAnchoredToRight
onPressAction={handlePressMenuItemForMenuView}
actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid}
shouldOpenOnLongPress={!isMenuPrimaryAction}
>
{isMenuPrimaryAction || isButton ? (
<TouchableOpacity style={buttonStyle} disabled={disabled} onPress={onPress} {...restProps}>
{children}
</TouchableOpacity>
) : (
children
)}
</MenuView>
</View>
<MenuView
title={title}
isAnchoredToRight
onPressAction={handlePressMenuItemForMenuView}
actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid}
shouldOpenOnLongPress={!isMenuPrimaryAction}
// @ts-ignore: its not in the types but it works
accessibilityLabel={props.accessibilityLabel}
accessibilityHint={props.accessibilityHint}
accessibilityRole={props.accessibilityRole}
accessibilityLanguage={language}
>
{isMenuPrimaryAction || isButton ? (
<TouchableOpacity style={buttonStyle} disabled={disabled} onPress={onPress} {...restProps}>
{children}
</TouchableOpacity>
) : (
children
)}
</MenuView>
);
};

View file

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Clipboard from '@react-native-clipboard/clipboard';
import { Linking, StyleSheet, View } from 'react-native';
import { Linking, View } from 'react-native';
import Lnurl from '../class/lnurl';
import { LightningTransaction, Transaction } from '../class/wallets/types';
import TransactionExpiredIcon from '../components/icons/TransactionExpiredIcon';
@ -37,7 +37,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const { navigate } = useExtendedNavigation<NavigationProps>();
const menuRef = useRef<ToolTipMenuProps>();
const { txMetadata, counterpartyMetadata, wallets } = useStorage();
const { preferredFiatCurrency, language } = useSettings();
const { language } = useSettings();
const containerStyle = useMemo(
() => ({
backgroundColor: 'transparent',
@ -94,8 +94,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
} else {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, itemPriceUnit, preferredFiatCurrency]);
}, [item, itemPriceUnit]);
const rowTitleStyle = useMemo(() => {
let color = colors.successColor;
@ -126,73 +125,70 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
};
}, [item, colors.foregroundColor, colors.successColor]);
const avatar = useMemo(() => {
// is it lightning refill tx?
const determineTransactionTypeAndAvatar = () => {
if (item.category === 'receive' && item.confirmations! < 3) {
return (
<View style={styles.iconWidth}>
<TransactionPendingIcon />
</View>
);
return {
label: loc.transactions.pending_transaction,
icon: <TransactionPendingIcon />,
};
}
if (item.type && item.type === 'bitcoind_tx') {
return (
<View style={styles.iconWidth}>
<TransactionOnchainIcon />
</View>
);
return {
label: loc.transactions.onchain,
icon: <TransactionOnchainIcon />,
};
}
if (item.type === 'paid_invoice') {
// is it lightning offchain payment?
return (
<View style={styles.iconWidth}>
<TransactionOffchainIcon />
</View>
);
return {
label: loc.transactions.offchain,
icon: <TransactionOffchainIcon />,
};
}
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (!item.ispaid) {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration < now) {
return (
<View style={styles.iconWidth}>
<TransactionExpiredIcon />
</View>
);
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (!item.ispaid && invoiceExpiration < now) {
return {
label: loc.transactions.expired_transaction,
icon: <TransactionExpiredIcon />,
};
} else {
return (
<View style={styles.iconWidth}>
<TransactionOffchainIncomingIcon />
</View>
);
return {
label: loc.transactions.incoming_transaction,
icon: <TransactionOffchainIncomingIcon />,
};
}
}
if (!item.confirmations) {
return (
<View style={styles.iconWidth}>
<TransactionPendingIcon />
</View>
);
return {
label: loc.transactions.pending_transaction,
icon: <TransactionPendingIcon />,
};
} else if (item.value! < 0) {
return (
<View style={styles.iconWidth}>
<TransactionOutgoingIcon />
</View>
);
return {
label: loc.transactions.outgoing_transaction,
icon: <TransactionOutgoingIcon />,
};
} else {
return (
<View style={styles.iconWidth}>
<TransactionIncomingIcon />
</View>
);
return {
label: loc.transactions.incoming_transaction,
icon: <TransactionIncomingIcon />,
};
}
}, [item]);
};
const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar();
const amountWithUnit = useMemo(() => {
const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
return `${amount}${unit}`;
}, [item.value, itemPriceUnit]);
useEffect(() => {
setSubtitleNumberOfLines(1);
@ -234,13 +230,11 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, wallets]);
}, [item, wallets, navigate, walletID]);
const handleOnExpandNote = useCallback(() => {
setSubtitleNumberOfLines(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subtitle]);
}, []);
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
@ -336,10 +330,18 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
}
return actions as Action[] | Action[][];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines, txMetadata]);
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
return (
<ToolTipMenu isButton actions={toolTipActions} onPressMenuItem={onToolTipPress} onPress={onPress}>
<ToolTipMenu
isButton
actions={toolTipActions}
onPressMenuItem={onToolTipPress}
onPress={onPress}
accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`}
accessibilityRole="button"
accessibilityState={{ expanded: subtitleNumberOfLines === 0 }}
>
<ListItem
leftAvatar={avatar}
title={title}
@ -382,7 +384,3 @@ const actionIcons = {
iconValue: 'note.text',
},
};
const styles = StyleSheet.create({
iconWidth: { width: 25 },
});

View file

@ -30,6 +30,7 @@ export interface ToolTipMenuProps {
style?: ViewStyle | ViewStyle[];
accessibilityLabel?: string;
accessibilityHint?: string;
accessibilityState?: object;
buttonStyle?: ViewStyle | ViewStyle[];
onMenuWillShow?: () => void;
onMenuWillHide?: () => void;

View file

@ -362,6 +362,12 @@
"transaction_saved": "Saved",
"details_show_in_block_explorer": "View in Block Explorer",
"details_title": "Transaction",
"incoming_transaction": "Incoming Transaction",
"outgoing_transaction": "Outgoing Transaction",
"expired_transaction": "Expired Transaction",
"pending_transaction": "Pending Transaction",
"offchain": "Offchain",
"onchain": "Onchain",
"details_to": "Output",
"enable_offline_signing": "This wallet is not being used in conjunction with an offline signing. Would you wish to enable it now?",
"list_conf": "Conf: {number}",
@ -373,6 +379,7 @@
"eta_1d": "ETA: In ~1 day",
"view_wallet": "View {walletLabel}",
"list_title": "Transactions",
"transaction": "Transaction",
"open_url_error": "Unable to open the link with the default browser. Please change your default browser and try again.",
"rbf_explain": "We will replace this transaction with one with a higher fee so that it will be mined faster. This is called RBF—Replace by Fee.",
"rbf_title": "Bump Fee (RBF)",