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

View file

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

View file

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

View file

@ -362,6 +362,12 @@
"transaction_saved": "Saved", "transaction_saved": "Saved",
"details_show_in_block_explorer": "View in Block Explorer", "details_show_in_block_explorer": "View in Block Explorer",
"details_title": "Transaction", "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", "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?", "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}", "list_conf": "Conf: {number}",
@ -373,6 +379,7 @@
"eta_1d": "ETA: In ~1 day", "eta_1d": "ETA: In ~1 day",
"view_wallet": "View {walletLabel}", "view_wallet": "View {walletLabel}",
"list_title": "Transactions", "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.", "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_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)", "rbf_title": "Bump Fee (RBF)",