Skip to main content

BTDeviceItem

A compound component for rendering individual medical device cards with customizable layout, status indicators, and interactive elements.

Overview

The BTDeviceItem component follows a compound component pattern, providing maximum flexibility for creating custom device card layouts. It includes multiple sub-components that can be composed together to create rich, interactive device interfaces.

Compound Component Architecture

BTDeviceItem                    // Main container (TouchableOpacity)
├── BTDeviceItem.TopContainer // Top section layout
│ ├── BTDeviceItem.Content // Text content area
│ │ ├── BTDeviceItem.Title // Device name
│ │ ├── BTDeviceItem.SubTitle // Device description
│ │ └── BTDeviceItem.Status // Connection status
│ └── BTDeviceItem.Image // Device image
├── BTDeviceItem.Divider // Separator line
└── BTDeviceItem.BottomContainer // Bottom section layout

Component API

BTDeviceItem (Main Container)

interface BTDeviceItemProps extends TouchableOpacityProps {
children: React.ReactNode;
}

The main container is a TouchableOpacity with theme-aware styling including shadows, borders, and background colors.

Sub-Components

ComponentBasePropsDescription
TopContainerViewViewPropsTop section with horizontal layout
ContentViewViewPropsText content area with vertical layout
TitleTextTextProps<T>Primary device title with theme colors
SubTitleTextTextProps<T>Secondary device description
StatusTextBTDeviceItemStatusProps<T>Status with color coding
ImageOptimizedImageOptimizedImagePropsDevice image (65x65)
DividerDividerPaper DividerHorizontal separator line
BottomContainerViewViewPropsBottom section with horizontal layout

Basic Usage

Simple Device Card

import React from "react";
import { BTDeviceItem } from "@modules/bt-device";
import { DeviceStatus } from "@modules/bt-management";

const SimpleDeviceCard = () => {
return (
<BTDeviceItem onPress={() => console.log("Device pressed")}>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title>Blood Pressure Monitor</BTDeviceItem.Title>
<BTDeviceItem.SubTitle>Wellue BP2 Armfit+</BTDeviceItem.SubTitle>
<BTDeviceItem.Status status={DeviceStatus.CONNECTED} />
</BTDeviceItem.Content>
<BTDeviceItem.Image uri="device-image-uri" />
</BTDeviceItem.TopContainer>
</BTDeviceItem>
);
};

Complete Device Card

import React from "react";
import { ArrowRight2 } from "iconsax-react-nativejs";
import { BTDeviceItem } from "@modules/bt-device";
import { useAppTheme } from "@modules/theme";

const CompleteDeviceCard = ({ device, onPress, onInstructionsPress }) => {
const theme = useAppTheme();

return (
<BTDeviceItem
onPress={() => onPress(device)}
accessibilityLabel={`${device.title} - ${device.status}`}
>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title>{device.title}</BTDeviceItem.Title>
<BTDeviceItem.SubTitle>{device.subtitle}</BTDeviceItem.SubTitle>
<BTDeviceItem.Status status={device.status} />
</BTDeviceItem.Content>
<BTDeviceItem.Image uri={device.deviceImageUri} />
</BTDeviceItem.TopContainer>

<BTDeviceItem.Divider />

<BTDeviceItem.BottomContainer>
<BTDeviceItem.SubTitle>View Instructions</BTDeviceItem.SubTitle>
<ArrowRight2 size={20} color={theme.colors.onSurfaceDisabled} />
</BTDeviceItem.BottomContainer>
</BTDeviceItem>
);
};

Advanced Usage

Custom Layout Variations

import React from "react";
import { View } from "react-native";
import { BTDeviceItem } from "@modules/bt-device";

// Minimal card
const MinimalDeviceCard = ({ device }) => (
<BTDeviceItem onPress={() => console.log("Pressed")}>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title>{device.title}</BTDeviceItem.Title>
<BTDeviceItem.Status status={device.status} />
</BTDeviceItem.Content>
</BTDeviceItem.TopContainer>
</BTDeviceItem>
);

// Image-focused card
const ImageFocusedCard = ({ device }) => (
<BTDeviceItem onPress={() => console.log("Pressed")}>
<View style={{ alignItems: "center", padding: 16 }}>
<BTDeviceItem.Image uri={device.deviceImageUri} />
<BTDeviceItem.Title style={{ marginTop: 8 }}>
{device.title}
</BTDeviceItem.Title>
<BTDeviceItem.Status status={device.status} />
</View>
</BTDeviceItem>
);

// Horizontal layout
const HorizontalDeviceCard = ({ device }) => (
<BTDeviceItem onPress={() => console.log("Pressed")}>
<BTDeviceItem.TopContainer style={{ alignItems: "center" }}>
<BTDeviceItem.Image uri={device.deviceImageUri} />
<BTDeviceItem.Content style={{ marginLeft: 12 }}>
<BTDeviceItem.Title>{device.title}</BTDeviceItem.Title>
<BTDeviceItem.SubTitle>{device.subtitle}</BTDeviceItem.SubTitle>
</BTDeviceItem.Content>
<BTDeviceItem.Status status={device.status} />
</BTDeviceItem.TopContainer>
</BTDeviceItem>
);

Status Indicator Customization

import React from "react";
import { View } from "react-native";
import { BTDeviceItem } from "@modules/bt-device";
import { DeviceStatus } from "@modules/bt-management";

const StatusIndicatorCard = ({ device }) => {
const getStatusIcon = (status: DeviceStatus) => {
switch (status) {
case DeviceStatus.CONNECTED:
return "🟢";
case DeviceStatus.MEASURING:
return "🟡";
case DeviceStatus.LOW_BATTERY:
return "🔴";
default:
return "⚪";
}
};

return (
<BTDeviceItem onPress={() => console.log("Pressed")}>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<BTDeviceItem.Title>{device.title}</BTDeviceItem.Title>
<Text style={{ marginLeft: 8, fontSize: 16 }}>
{getStatusIcon(device.status)}
</Text>
</View>
<BTDeviceItem.SubTitle>{device.subtitle}</BTDeviceItem.SubTitle>
<BTDeviceItem.Status status={device.status} />
</BTDeviceItem.Content>
<BTDeviceItem.Image uri={device.deviceImageUri} />
</BTDeviceItem.TopContainer>
</BTDeviceItem>
);
};

Interactive Elements

import React from "react";
import { Button, View } from "react-native";
import { BTDeviceItem } from "@modules/bt-device";

const InteractiveDeviceCard = ({
device,
onConnect,
onDisconnect,
onInstructions,
}) => {
const isConnected = device.status === DeviceStatus.CONNECTED;

return (
<BTDeviceItem>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title>{device.title}</BTDeviceItem.Title>
<BTDeviceItem.SubTitle>{device.subtitle}</BTDeviceItem.SubTitle>
<BTDeviceItem.Status status={device.status} />
</BTDeviceItem.Content>
<BTDeviceItem.Image uri={device.deviceImageUri} />
</BTDeviceItem.TopContainer>

<BTDeviceItem.Divider />

<View style={{ padding: 16, gap: 12 }}>
<Button
title={isConnected ? "Disconnect" : "Connect"}
onPress={isConnected ? onDisconnect : onConnect}
/>
<Button
title="View Instructions"
onPress={onInstructions}
variant="outline"
/>
</View>
</BTDeviceItem>
);
};

Sub-Component Details

BTDeviceItem.Status

Special component for displaying device connection status with color coding.

interface BTDeviceItemStatusProps<T extends MD3TypescaleKey> {
status: DeviceStatus;
} & Omit<TextProps<T>, "children">;

Status Color Mapping:

StatusColorLabel
CONNECTEDtheme.colors.success"Connected"
DISCONNECTEDtheme.colors.error"Disconnected"
MEASURINGtheme.colors.warning"Measuring"
LOW_BATTERYtheme.colors.error"Low Battery"
// Usage with custom styling
<BTDeviceItem.Status
status={DeviceStatus.CONNECTED}
style={{ fontSize: 16, fontWeight: "bold" }}
/>

BTDeviceItem.Image

Optimized image component with fixed dimensions for consistent layout.

interface OptimizedImageProps {
uri: string;
width?: number; // Default: 65
height?: number; // Default: 65
style?: ImageStyle;
}
// Custom sized image
<BTDeviceItem.Image
uri={device.deviceImageUri}
width={80}
height={80}
style={{ borderRadius: 8 }}
/>

BTDeviceItem.Title and SubTitle

Text components with theme-aware styling.

// Title with custom styling
<BTDeviceItem.Title style={{ fontSize: 20, fontWeight: '800' }}>
Custom Device Name
</BTDeviceItem.Title>

// SubTitle with secondary color
<BTDeviceItem.SubTitle style={{ color: theme.colors.onSurfaceVariant }}>
Device Model Number
</BTDeviceItem.SubTitle>

Styling and Theming

Theme Integration

All components automatically use the app theme for consistent styling:

// Main container styling (automatic)
{
borderRadius: theme.borderRadius(4),
shadowColor: theme.colors.shadow,
backgroundColor: theme.colors.surface,
borderColor: theme.colors.outlineVariant,
// ... shadow and elevation styles
}

// Title styling (automatic)
{
fontSize: 18,
fontFamily: "DMSans_700Bold",
color: theme.colors.primary
}

// SubTitle styling (automatic)
{
fontSize: 14,
fontFamily: "DMSans_500Medium"
}

Custom Styling

import { StyleSheet } from "react-native";
import { useAppTheme } from "@modules/theme";

const CustomStyledCard = () => {
const theme = useAppTheme();

return (
<BTDeviceItem style={styles.customCard}>
<BTDeviceItem.TopContainer style={styles.topSection}>
<BTDeviceItem.Content style={styles.contentArea}>
<BTDeviceItem.Title style={styles.customTitle}>
Custom Device
</BTDeviceItem.Title>
<BTDeviceItem.SubTitle style={styles.customSubtitle}>
With custom styling
</BTDeviceItem.SubTitle>
</BTDeviceItem.Content>
</BTDeviceItem.TopContainer>
</BTDeviceItem>
);
};

const styles = StyleSheet.create({
customCard: {
borderRadius: 16,
marginHorizontal: 8,
backgroundColor: "#f8f9fa",
},
topSection: {
padding: 20,
},
contentArea: {
alignItems: "center",
},
customTitle: {
fontSize: 22,
color: "#2c3e50",
},
customSubtitle: {
fontSize: 16,
color: "#7f8c8d",
marginTop: 4,
},
});

Accessibility

Screen Reader Support

<BTDeviceItem
accessible={true}
accessibilityRole="button"
accessibilityLabel={`${device.title}, ${device.subtitle}, Status: ${device.status}`}
accessibilityHint="Tap to connect to this device"
onPress={handlePress}
>
{/* Content */}
</BTDeviceItem>

Accessibility Best Practices

const AccessibleDeviceCard = ({ device }) => {
return (
<BTDeviceItem
accessible={true}
accessibilityRole="button"
accessibilityState={{
selected: device.status === DeviceStatus.CONNECTED,
}}
accessibilityValue={{
text: `Connection status: ${device.status}`,
}}
>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title
accessible={false} // Grouped with parent
>
{device.title}
</BTDeviceItem.Title>
<BTDeviceItem.SubTitle
accessible={false} // Grouped with parent
>
{device.subtitle}
</BTDeviceItem.SubTitle>
<BTDeviceItem.Status
status={device.status}
accessible={false} // Grouped with parent
/>
</BTDeviceItem.Content>
<BTDeviceItem.Image
uri={device.deviceImageUri}
accessible={false} // Decorative image
/>
</BTDeviceItem.TopContainer>
</BTDeviceItem>
);
};

Animation and Interactions

Press Animations

import { Animated } from "react-native";

const AnimatedDeviceCard = ({ device }) => {
const scaleValue = useRef(new Animated.Value(1)).current;

const handlePressIn = () => {
Animated.spring(scaleValue, {
toValue: 0.95,
useNativeDriver: true,
}).start();
};

const handlePressOut = () => {
Animated.spring(scaleValue, {
toValue: 1,
useNativeDriver: true,
}).start();
};

return (
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
<BTDeviceItem
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={() => console.log("Pressed")}
>
{/* Content */}
</BTDeviceItem>
</Animated.View>
);
};

Status Animations

import { useEffect, useRef } from "react";
import { Animated } from "react-native";

const AnimatedStatusCard = ({ device }) => {
const pulseValue = useRef(new Animated.Value(1)).current;

useEffect(() => {
if (device.status === DeviceStatus.MEASURING) {
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(pulseValue, {
toValue: 1.1,
duration: 800,
useNativeDriver: true,
}),
Animated.timing(pulseValue, {
toValue: 1,
duration: 800,
useNativeDriver: true,
}),
]),
);
pulse.start();

return () => pulse.stop();
}
}, [device.status]);

return (
<BTDeviceItem>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title>{device.title}</BTDeviceItem.Title>
<Animated.View style={{ transform: [{ scale: pulseValue }] }}>
<BTDeviceItem.Status status={device.status} />
</Animated.View>
</BTDeviceItem.Content>
</BTDeviceItem.TopContainer>
</BTDeviceItem>
);
};

Testing

Component Testing

import React from "react";
import { render, fireEvent } from "@testing-library/react-native";
import { BTDeviceItem } from "@modules/bt-device";
import { DeviceStatus } from "@modules/bt-management";

describe("BTDeviceItem", () => {
const mockDevice = {
title: "Test Device",
subtitle: "Test Model",
status: DeviceStatus.CONNECTED,
deviceImageUri: "test-image-uri",
};

it("renders all sub-components correctly", () => {
const { getByText } = render(
<BTDeviceItem>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title>{mockDevice.title}</BTDeviceItem.Title>
<BTDeviceItem.SubTitle>{mockDevice.subtitle}</BTDeviceItem.SubTitle>
<BTDeviceItem.Status status={mockDevice.status} />
</BTDeviceItem.Content>
<BTDeviceItem.Image uri={mockDevice.deviceImageUri} />
</BTDeviceItem.TopContainer>
</BTDeviceItem>,
);

expect(getByText("Test Device")).toBeTruthy();
expect(getByText("Test Model")).toBeTruthy();
expect(getByText("Connected")).toBeTruthy();
});

it("handles press events correctly", () => {
const mockOnPress = jest.fn();

const { getByTestId } = render(
<BTDeviceItem testID="device-item" onPress={mockOnPress}>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Title>Test Device</BTDeviceItem.Title>
</BTDeviceItem.TopContainer>
</BTDeviceItem>,
);

fireEvent.press(getByTestId("device-item"));
expect(mockOnPress).toHaveBeenCalled();
});

it("applies custom styling correctly", () => {
const customStyle = { backgroundColor: "red" };

const { getByTestId } = render(
<BTDeviceItem testID="device-item" style={customStyle}>
<BTDeviceItem.Title>Test</BTDeviceItem.Title>
</BTDeviceItem>,
);

const item = getByTestId("device-item");
expect(item.props.style).toContainEqual(
expect.objectContaining(customStyle),
);
});

it("displays correct status colors", () => {
const { getByText, rerender } = render(
<BTDeviceItem.Status status={DeviceStatus.CONNECTED} />,
);

const connectedStatus = getByText("Connected");
expect(connectedStatus.props.style).toContainEqual(
expect.objectContaining({
color: expect.any(String), // Success color
}),
);

rerender(<BTDeviceItem.Status status={DeviceStatus.DISCONNECTED} />);

const disconnectedStatus = getByText("Disconnected");
expect(disconnectedStatus.props.style).toContainEqual(
expect.objectContaining({
color: expect.any(String), // Error color
}),
);
});
});

Accessibility Testing

import { render } from "@testing-library/react-native";
import { BTDeviceItem } from "@modules/bt-device";

describe("BTDeviceItem Accessibility", () => {
it("has proper accessibility properties", () => {
const { getByRole } = render(
<BTDeviceItem
accessibilityLabel="Test device, connected"
accessibilityRole="button"
>
<BTDeviceItem.Title>Test Device</BTDeviceItem.Title>
</BTDeviceItem>,
);

const button = getByRole("button");
expect(button).toHaveAccessibilityLabel("Test device, connected");
});

it("groups child elements for screen readers", () => {
const { getByLabelText } = render(
<BTDeviceItem accessibilityLabel="Complete device info" accessible={true}>
<BTDeviceItem.Title accessible={false}>Device</BTDeviceItem.Title>
<BTDeviceItem.SubTitle accessible={false}>Model</BTDeviceItem.SubTitle>
</BTDeviceItem>,
);

expect(getByLabelText("Complete device info")).toBeTruthy();
});
});

Common Patterns

Grid Layout

const DeviceGrid = ({ devices }) => {
return (
<FlatList
data={devices}
numColumns={2}
renderItem={({ item }) => (
<View style={{ flex: 1, margin: 8 }}>
<BTDeviceItem onPress={() => handlePress(item)}>
<View style={{ alignItems: "center", padding: 16 }}>
<BTDeviceItem.Image uri={item.deviceImageUri} />
<BTDeviceItem.Title style={{ marginTop: 8, textAlign: "center" }}>
{item.title}
</BTDeviceItem.Title>
<BTDeviceItem.Status status={item.status} />
</View>
</BTDeviceItem>
</View>
)}
/>
);
};
const DeviceCarousel = ({ devices }) => {
return (
<FlatList
data={devices}
horizontal
showsHorizontalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={{ width: 12 }} />}
renderItem={({ item }) => (
<BTDeviceItem style={{ width: 200 }} onPress={() => handlePress(item)}>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content>
<BTDeviceItem.Title numberOfLines={1}>
{item.title}
</BTDeviceItem.Title>
<BTDeviceItem.Status status={item.status} />
</BTDeviceItem.Content>
</BTDeviceItem.TopContainer>
</BTDeviceItem>
)}
/>
);
};

Status Dashboard

const StatusDashboard = ({ devices }) => {
const connectedCount = devices.filter(
(d) => d.status === DeviceStatus.CONNECTED,
).length;

return (
<View style={styles.dashboard}>
<Text style={styles.header}>
Connected: {connectedCount}/{devices.length}
</Text>

{devices.map((device, index) => (
<BTDeviceItem key={index} style={styles.dashboardItem}>
<BTDeviceItem.TopContainer>
<BTDeviceItem.Content style={{ flex: 1 }}>
<BTDeviceItem.Title>{device.title}</BTDeviceItem.Title>
</BTDeviceItem.Content>
<BTDeviceItem.Status status={device.status} />
</BTDeviceItem.TopContainer>
</BTDeviceItem>
))}
</View>
);
};

Best Practices

Component Design

  1. Composition: Use compound components for maximum flexibility
  2. Consistency: Maintain consistent layout patterns across device types
  3. Accessibility: Always include proper accessibility labels
  4. Performance: Memoize expensive calculations and event handlers

Styling

  1. Theme Usage: Always use theme values for colors, spacing, and typography
  2. Responsive Design: Test layouts on various screen sizes
  3. Visual Hierarchy: Use typography and color to establish clear hierarchy
  4. Touch Targets: Ensure touch targets meet minimum size requirements (44pt)

User Experience

  1. Loading States: Show loading indicators during state transitions
  2. Error States: Provide clear error messages and recovery options
  3. Feedback: Give immediate visual feedback for user interactions
  4. Context: Provide enough context for users to understand device capabilities

API Reference

BTDeviceItem

interface BTDeviceItemProps extends TouchableOpacityProps {
children: React.ReactNode;
}

BTDeviceItem.Status

interface BTDeviceItemStatusProps<T extends MD3TypescaleKey> {
status: DeviceStatus;
} & Omit<TextProps<T>, "children">;

Default Styling

The component includes built-in styling for elevation, shadows, borders, and colors that adapt to the app theme automatically.

Dependencies

  • react-native: Core React Native components
  • react-native-paper: Material Design components (Divider)
  • @modules/theme: App theme system
  • @modules/image: Optimized image components
  • @modules/bt-management: Device status enums