Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export default function HomeScreen() {
getItemLayout={getItemLayout}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 16 }}
accessibilityLabel="Cryptocurrency list"
accessibilityState={{ busy: isLoading }}
/>
</View>
);
Expand Down
6 changes: 6 additions & 0 deletions components/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ export function Text({
lightColor,
darkColor,
type = 'default',
accessibilityRole,
...rest
}: ThemedTextProps) {
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');

const resolvedRole =
accessibilityRole ??
(type === 'title' || type === 'subtitle' ? 'header' : undefined);

return (
<RNText
accessibilityRole={resolvedRole}
style={[
{ color },
type === 'default' ? styles.default : undefined,
Expand Down
7 changes: 5 additions & 2 deletions features/home/components/coin-logo.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Image } from 'expo-image';
import { StyleSheet } from 'react-native';
import type { AccessibilityProps } from 'react-native';

interface CoinLogoProps {
interface CoinLogoProps extends AccessibilityProps {
uri?: string;
size?: number;
}

export const CoinLogo = ({ uri, size = 32 }: CoinLogoProps) => (
export const CoinLogo = ({ uri, size = 32, ...accessibilityProps }: CoinLogoProps) => (
<Image
source={uri ? { uri } : null}
style={[styles.logo, { width: size, height: size, borderRadius: size / 2 }]}
contentFit="contain"
transition={150}
cachePolicy="disk"
accessibilityRole="image"
{...accessibilityProps}
/>
);

Expand Down
25 changes: 15 additions & 10 deletions features/home/components/coin-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { View } from '@/components/view';
import { Text } from '@/components/text';
import { CoinLogo } from '@/features/home/components/coin-logo';
import { CHANGE_COLORS, COIN_ITEM_HEIGHT } from '@/features/home/config';
import { formatChange, formatPrice } from '@/features/home/utils';
import { coinAccessibilityLabel, formatChange, formatPrice } from '@/features/home/utils';
import type { CmcCoin } from '@/types/cmc';

interface CoinRowProps {
Expand All @@ -17,16 +17,21 @@ export function CoinRow({ coin, logoUrl }: CoinRowProps) {
const changeColor = change >= 0 ? CHANGE_COLORS.positive : CHANGE_COLORS.negative;

return (
<View style={styles.coinRow}>
<Text style={styles.rank}>#{coin.cmc_rank}</Text>
<CoinLogo uri={logoUrl} size={32} />
<View style={styles.coinInfo}>
<Text type="defaultSemiBold">{coin.name}</Text>
<Text style={styles.symbol}>{coin.symbol}</Text>
<View
accessible={true}
accessibilityRole="text"
accessibilityLabel={coinAccessibilityLabel(coin)}
style={styles.coinRow}
>
<Text accessible={false} style={styles.rank}>#{coin.cmc_rank}</Text>
<CoinLogo accessible={false} uri={logoUrl} size={32} />
<View accessible={false} style={styles.coinInfo}>
<Text accessible={false} type="defaultSemiBold">{coin.name}</Text>
<Text accessible={false} style={styles.symbol}>{coin.symbol}</Text>
</View>
<View style={styles.coinPrices}>
<Text type="defaultSemiBold">{formatPrice(coin.quote.USD.price)}</Text>
<Text style={[styles.change, { color: changeColor }]}>
<View accessible={false} style={styles.coinPrices}>
<Text accessible={false} type="defaultSemiBold">{formatPrice(coin.quote.USD.price)}</Text>
<Text accessible={false} style={[styles.change, { color: changeColor }]}>
{formatChange(change)}
</Text>
</View>
Expand Down
35 changes: 26 additions & 9 deletions features/home/components/home-hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,26 @@ interface HomeHeroProps {
export function HomeHero({ coin, isLoading, isError, height, logoUrl }: HomeHeroProps) {
if (isLoading) {
return (
<View style={[styles.hero, { height: height * 0.25 }]}>
<Text style={styles.label}>Loading...</Text>
<View
accessible={true}
accessibilityLabel="Loading Bitcoin data"
accessibilityState={{ busy: true }}
style={[styles.hero, { height: height * 0.25 }]}
>
<Text accessible={false} style={styles.label}>Loading...</Text>
</View>
);
}

if (isError) {
return (
<View style={[styles.hero, { height: height * 0.25 }]}>
<Text style={styles.label}>Failed to load</Text>
<View
accessible={true}
accessibilityLabel="Failed to load Bitcoin data"
accessibilityLiveRegion="assertive"
style={[styles.hero, { height: height * 0.25 }]}
>
<Text accessible={false} style={styles.label}>Failed to load</Text>
</View>
);
}
Expand All @@ -36,13 +46,20 @@ export function HomeHero({ coin, isLoading, isError, height, logoUrl }: HomeHero

const change = coin.quote.USD.percent_change_24h;
const changeColor = change >= 0 ? CHANGE_COLORS.positive : CHANGE_COLORS.negative;
const direction = change >= 0 ? 'up' : 'down';
const changeAbs = Math.abs(change).toFixed(2);
const heroLabel = `${coin.name} featured. Price ${formatPrice(coin.quote.USD.price)}, ${direction} ${changeAbs}% today`;

return (
<View style={[styles.hero, { height: height * 0.25 }]}>
<CoinLogo uri={logoUrl} size={48} />
<Text style={styles.label}>{coin.name} ({coin.symbol})</Text>
<Text style={styles.price}>{formatPrice(coin.quote.USD.price)}</Text>
<Text style={[styles.change, { color: changeColor }]}>
<View
accessible={true}
accessibilityLabel={heroLabel}
style={[styles.hero, { height: height * 0.25 }]}
>
<CoinLogo accessible={false} uri={logoUrl} size={48} />
<Text accessible={false} style={styles.label}>{coin.name} ({coin.symbol})</Text>
<Text accessible={false} style={styles.price}>{formatPrice(coin.quote.USD.price)}</Text>
<Text accessible={false} style={[styles.change, { color: changeColor }]}>
{formatChange(change)} today
</Text>
</View>
Expand Down
8 changes: 6 additions & 2 deletions features/home/components/list-section-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ interface ListSectionHeaderProps {

export function ListSectionHeader({ borderColor }: ListSectionHeaderProps) {
return (
<View style={[styles.container, { borderBottomColor: borderColor }]}>
<Text style={styles.label}>All Cryptos</Text>
<View
accessibilityRole="header"
accessibilityLabel="All Cryptocurrencies section"
style={[styles.container, { borderBottomColor: borderColor }]}
>
<Text accessible={false} style={styles.label}>All Cryptos</Text>
</View>
);
}
Expand Down
9 changes: 9 additions & 0 deletions features/home/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { CmcCoin } from '@/types/cmc';

export function formatPrice(price: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
Expand All @@ -10,3 +12,10 @@ export function formatChange(change: number): string {
const sign = change >= 0 ? '+' : '';
return `${sign}${change.toFixed(2)}%`;
}

export function coinAccessibilityLabel(coin: CmcCoin): string {
const change = coin.quote.USD.percent_change_24h;
const direction = change >= 0 ? 'up' : 'down';
const changeAbs = Math.abs(change).toFixed(2);
return `${coin.name}, ${coin.symbol}, ranked ${coin.cmc_rank}, price ${formatPrice(coin.quote.USD.price)}, ${direction} ${changeAbs}% in the last 24 hours`;
}
Loading