mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-23 07:15:35 +01:00
FIX: Accessibility for Transaction rows
This commit is contained in:
parent
aeb4db00eb
commit
61ee3dfaba
4 changed files with 100 additions and 83 deletions
|
@ -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,13 +141,17 @@ 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}
|
||||||
|
accessibilityHint={props.accessibilityHint}
|
||||||
|
accessibilityRole={props.accessibilityRole}
|
||||||
|
accessibilityLanguage={language}
|
||||||
>
|
>
|
||||||
{isMenuPrimaryAction || isButton ? (
|
{isMenuPrimaryAction || isButton ? (
|
||||||
<TouchableOpacity style={buttonStyle} disabled={disabled} onPress={onPress} {...restProps}>
|
<TouchableOpacity style={buttonStyle} disabled={disabled} onPress={onPress} {...restProps}>
|
||||||
|
@ -149,7 +161,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
</MenuView>
|
</MenuView>
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 (invoiceExpiration < now) {
|
if (!item.ispaid && invoiceExpiration < now) {
|
||||||
return (
|
return {
|
||||||
<View style={styles.iconWidth}>
|
label: loc.transactions.expired_transaction,
|
||||||
<TransactionExpiredIcon />
|
icon: <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 },
|
|
||||||
});
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)",
|
||||||
|
|
Loading…
Add table
Reference in a new issue