Skip to main content

ContentDetail

A comprehensive compound React component for displaying detailed content views with FHIR Composition integration, rich HTML rendering, metadata display, and extensible content sections. Built as a scrollable container with theme integration and accessibility support.

Overview

The ContentDetail component provides a complete solution for displaying detailed content such as articles, clinical guidelines, patient education materials, and other rich content. It features deep integration with FHIR Composition resources and supports complex HTML rendering with custom image optimization.

Features

  • FHIR Integration: Native support for FHIR Composition resources with automatic data extraction
  • Rich HTML Rendering: Advanced HTML content rendering with custom elements and styling
  • Compound Components: Flexible sub-components for different content sections
  • Metadata Display: Comprehensive metadata rendering (author, date, reading time)
  • Tag System: Content categorization with interactive tags
  • External Resources: Support for external resource links with web browser integration
  • Context Provider: Composition data available throughout component tree
  • Theme Integration: Seamless theme adaptation and Material Design 3 support
  • Accessibility: Full accessibility support with proper semantic structure
  • Internationalization: Built-in localization support

Compound Components

ContentDetail (Root)

Main scrollable container that provides FHIR Composition context to all child components.

ContentDetail.Header

Header section container for title, metadata, and other header content.

ContentDetail.Title

Main content title that automatically extracts from FHIR Composition or accepts custom content.

ContentDetail.Metadata

Metadata container with sub-components for author, date, reading time, and separators.

ContentDetail.Body

Main content body container for rich content and other body elements.

ContentDetail.RichContent

Advanced HTML content renderer with custom image optimization and extensible rendering.

ContentDetail.Footer

Footer section for tags, resource links, and other footer content.

ContentDetail.Tags

Tag display component with interactive tag selection and filtering.

ContentDetail.Resource

External resource link component with automatic web browser integration.

Props

ContentDetail (Root)

Extends ScrollViewProps:

PropTypeDefaultDescription
compositionCompositionundefinedFHIR Composition resource for content data
childrenReactNoderequiredContent detail sub-components
contentContainerStyleViewStyleundefinedAdditional styles for scroll content container

ContentDetail.Title

Extends TextProps from react-native-paper:

PropTypeDefaultDescription
childrenstringundefinedCustom title text (overrides composition title)

Component Behavior

The ContentDetail component provides comprehensive content rendering with these behaviors:

  • Automatic Data Extraction: Extracts title, content, metadata, and tags from FHIR Composition resources
  • Context Provision: Makes composition data available to all child components through React Context
  • Scroll Container: Provides a scrollable container with theme-based padding and spacing
  • HTML Rendering: Automatically renders HTML content with optimized images and custom elements
  • Metadata Processing: Extracts and formats author, date, and reading time from composition
  • Tag Management: Displays content tags as interactive chips when available
  • Resource Linking: Handles external resource links with web browser integration

Localization

The ContentDetail component uses the following localization keys:

{
"content": {
"metadata": {
"by": "By",
"on": "on",
"readingTime": "min read"
},
"error": {
"loadFailed": "Content could not be loaded. Please try again.",
"linkFailed": "Unable to open link. Please check your internet connection."
}
}
}

Usage with localization:

import { ContentDetail } from "@ovok/native";
import * as React from "react";
import { useTranslation } from "react-i18next";

const composition = {
resourceType: "Composition",
title: "Managing Your Heart Health",
status: "final",
date: "2024-12-20T10:00:00Z",
author: [{ display: "Dr. Sarah Wilson" }],
section: [
{
text: {
div: "<h2>Introduction</h2><p>Heart health is crucial for overall well-being. Simple lifestyle changes can make a significant difference.</p>",
},
},
],
meta: {
tag: [
{ code: "cardiology", display: "Cardiology" },
{ code: "patient-education", display: "Patient Education" },
],
},
id: "localized-content-detail",
};

const LocalizedContentDetail = () => {
const { t } = useTranslation();

return (
<ContentDetail
composition={composition}
accessibilityLabel={t("content.detail.accessibility", {
title: composition?.title,
})}
>
<ContentDetail.Header>
<ContentDetail.Title />
<ContentDetail.Metadata>
<ContentDetail.Metadata.Author />
<ContentDetail.Metadata.Separator />
<ContentDetail.Metadata.Date />
</ContentDetail.Metadata>
</ContentDetail.Header>

<ContentDetail.Body>
<ContentDetail.RichContent />
</ContentDetail.Body>
</ContentDetail>
);
};

export default LocalizedContentDetail;

ContentDetail.RichContent

Extends RenderHTMLProps:

PropTypeDefaultDescription
contentstringundefinedCustom HTML content (overrides composition content)

ContentDetail.Tags

Extends ViewProps:

PropTypeDefaultDescription
tagsCoding[]undefinedCustom tags array (overrides composition tags)
onTagPress(tag: Coding) => voidundefinedHandler for tag press interactions
chipPropsPartial<ChipProps>undefinedAdditional props for tag chips

ContentDetail.Metadata Components

All metadata components extend TextProps and support:

PropTypeDefaultDescription
childrenReactNodeundefinedCustom content (overrides auto-extracted data)

Styling

Theme Colors Used

The ContentDetail component uses the following theme values that can be changed globally:

  • theme.colors.onSurface - Text color for content
  • theme.colors.blue[500] - Resource link color
  • theme.spacing(4) - Container padding and gap
  • theme.spacing(3) - Section gaps
  • theme.spacing(2) - Metadata item gaps

Custom Styling

Container Styling

<ContentDetail
composition={composition}
contentContainerStyle={{
paddingHorizontal: 20,
paddingVertical: 16,
backgroundColor: "#f9f9f9",
}}
>
<ContentDetail.Title />
</ContentDetail>

Rich Content Styling

<ContentDetail.RichContent
baseStyle={{
fontSize: 18,
lineHeight: 28,
color: "#333",
}}
customHTMLElementModels={{
blockquote: HTMLElementModel.fromCustomModel({
tagName: "blockquote",
contentModel: HTMLContentModel.block,
}),
}}
/>

Tag Styling

<ContentDetail.Tags
chipProps={{
mode: "outlined",
style: { marginRight: 8, marginBottom: 4 },
}}
style={{
flexDirection: "row",
flexWrap: "wrap",
marginTop: 16,
}}
/>

Usage Patterns

Basic Content Detail

import { ContentDetail } from "@ovok/native";
import * as React from "react";

const composition = {
resourceType: "Composition",
title: "Introduction to Mobile Health",
status: "final",
date: "2024-12-20T10:00:00Z",
author: [{ display: "Dr. Sarah Wilson" }],
section: [
{
text: {
div: "<h2>Getting Started</h2><p>Mobile health applications are revolutionizing healthcare delivery by making medical information more accessible than ever before.</p>",
},
},
],
meta: {
tag: [
{ code: "mobile-health", display: "Mobile Health" },
{ code: "patient-education", display: "Patient Education" },
],
},
id: "basic-content-example",
};

const BasicContentDetail = () => {
return (
<ContentDetail composition={composition}>
<ContentDetail.Header>
<ContentDetail.Title />
<ContentDetail.Metadata>
<ContentDetail.Metadata.Date />
<ContentDetail.Metadata.Separator />
<ContentDetail.Metadata.Author />
</ContentDetail.Metadata>
</ContentDetail.Header>

<ContentDetail.Body>
<ContentDetail.RichContent />
</ContentDetail.Body>

<ContentDetail.Footer>
<ContentDetail.Tags />
</ContentDetail.Footer>
</ContentDetail>
);
};

export default BasicContentDetail;

Healthcare Article

<ContentDetail composition={medicalArticle}>
<ContentDetail.Header>
<ContentDetail.Title />
<ContentDetail.Metadata>
<ContentDetail.Metadata.Author />
<ContentDetail.Metadata.Separator />
<ContentDetail.Metadata.Date />
<ContentDetail.Metadata.Separator />
<ContentDetail.Metadata.ReadingTime />
</ContentDetail.Metadata>
</ContentDetail.Header>

<ContentDetail.Body>
<ContentDetail.RichContent />
</ContentDetail.Body>

<ContentDetail.Footer>
<ContentDetail.Tags onTagPress={filterByMedicalTag} />
<ContentDetail.Resource />
</ContentDetail.Footer>
</ContentDetail>

Patient Education Material

<ContentDetail composition={educationMaterial}>
<ContentDetail.Header>
<ContentDetail.Title />
<View style={styles.patientInfo}>
<Icon name="info" size={16} />
<Text variant="bodySmall">Patient Education Material</Text>
</View>
<ContentDetail.Metadata>
<ContentDetail.Metadata.ReadingTime />
</ContentDetail.Metadata>
</ContentDetail.Header>

<ContentDetail.Body>
<ContentDetail.RichContent baseStyle={styles.patientEducationText} />
</ContentDetail.Body>

<ContentDetail.Footer>
<ContentDetail.Tags />
<ContentDetail.Resource />
<Button
mode="contained"
onPress={printForPatient}
style={{ marginTop: 16 }}
>
Print for Patient
</Button>
</ContentDetail.Footer>
</ContentDetail>

Clinical Guideline

<ContentDetail composition={clinicalGuideline}>
<ContentDetail.Header>
<ContentDetail.Title />
<ContentDetail.Metadata>
<ContentDetail.Metadata.Item>
Guideline Version 2.1
</ContentDetail.Metadata.Item>
<ContentDetail.Metadata.Separator />
<ContentDetail.Metadata.Date />
</ContentDetail.Metadata>
</ContentDetail.Header>

<ContentDetail.Body>
<ContentDetail.RichContent
customHTMLElementModels={{
recommendation: HTMLElementModel.fromCustomModel({
tagName: "recommendation",
contentModel: HTMLContentModel.block,
}),
}}
renderers={{
recommendation: ({ tnode }) => (
<View style={styles.recommendation}>
<Icon name="lightbulb" size={20} color="orange" />
<Text style={styles.recommendationText}>
{domNodeToHTMLString(tnode)}
</Text>
</View>
),
}}
/>
</ContentDetail.Body>

<ContentDetail.Footer>
<ContentDetail.Tags onTagPress={filterGuidelines} />
<ContentDetail.Resource />
</ContentDetail.Footer>
</ContentDetail>

News Article

<ContentDetail composition={newsArticle}>
<ContentDetail.Header>
<ContentDetail.Title />
<ContentDetail.Metadata>
<ContentDetail.Metadata.Item>
{newsArticle.source}
</ContentDetail.Metadata.Item>
<ContentDetail.Metadata.Separator />
<ContentDetail.Metadata.Date />
</ContentDetail.Metadata>
</ContentDetail.Header>

<ContentDetail.Body>
<ContentDetail.RichContent />

{newsArticle.relatedArticles?.length > 0 && (
<View style={styles.relatedSection}>
<Text variant="titleMedium">Related Articles</Text>
{newsArticle.relatedArticles.map((article) => (
<TouchableOpacity
key={article.id}
onPress={() => openArticle(article)}
>
<Text variant="bodyMedium">{article.title}</Text>
</TouchableOpacity>
))}
</View>
)}
</ContentDetail.Body>

<ContentDetail.Footer>
<ContentDetail.Tags onTagPress={filterByTag} />
<ShareButton article={newsArticle} />
</ContentDetail.Footer>
</ContentDetail>

Custom Content Without FHIR

<ContentDetail>
<ContentDetail.Header>
<ContentDetail.Title>Custom Article Title</ContentDetail.Title>
<ContentDetail.Metadata>
<ContentDetail.Metadata.Item>John Doe</ContentDetail.Metadata.Item>
<ContentDetail.Metadata.Separator />
<ContentDetail.Metadata.Item>March 15, 2024</ContentDetail.Metadata.Item>
<ContentDetail.Metadata.Separator />
<ContentDetail.Metadata.Item>5 min read</ContentDetail.Metadata.Item>
</ContentDetail.Metadata>
</ContentDetail.Header>

<ContentDetail.Body>
<ContentDetail.RichContent content={customHtmlContent} />
</ContentDetail.Body>

<ContentDetail.Footer>
<ContentDetail.Tags tags={customTags} onTagPress={handleTagPress} />
</ContentDetail.Footer>
</ContentDetail>

FHIR Composition Integration

Automatic Data Extraction

The component automatically extracts data from FHIR Composition resources:

const composition = {
title: "Understanding Diabetes Management",
date: "2024-03-15T10:00:00Z",
author: [{ display: "Dr. Sarah Johnson" }],
meta: {
tag: [
{ code: "diabetes", display: "Diabetes" },
{ code: "patient-education", display: "Patient Education" },
],
},
section: [
{
text: {
div: "<h2>Introduction</h2><p>Diabetes management requires a comprehensive approach including diet, exercise, and medication adherence.</p>",
},
},
],
};

<ContentDetail composition={composition}>
{/* Components automatically use composition data */}
</ContentDetail>;

Context Usage

Access composition data in custom components:

import { useComposition } from "../context/hooks/use-composition";

function CustomMetadata() {
const { composition } = useComposition();

return (
<Text>
Last updated: {format(new Date(composition?.meta?.lastUpdated), "PPP")}
</Text>
);
}

<ContentDetail composition={composition}>
<ContentDetail.Header>
<ContentDetail.Title />
<CustomMetadata />
</ContentDetail.Header>
</ContentDetail>;

Rich Content Rendering

Custom HTML Elements

<ContentDetail.RichContent
customHTMLElementModels={{
"drug-info": HTMLElementModel.fromCustomModel({
tagName: "drug-info",
contentModel: HTMLContentModel.mixed,
}),
}}
renderers={{
"drug-info": ({ tnode }) => (
<View style={styles.drugInfo}>
<Icon name="pill" size={16} />
<Text>{tnode.children[0]?.data}</Text>
</View>
),
}}
/>

Image Optimization

// Custom image renderer with optimization
const customRenderers = {
img: ({ tnode }) => {
const { src, width, height, alt } = tnode.attributes;

return (
<OptimizedImage
uri={src}
width={width ? Number(width) : "100%"}
height={height ? Number(height) : undefined}
alt={alt}
style={styles.contentImage}
/>
);
},
};

<ContentDetail.RichContent renderers={customRenderers} />;

Accessibility

Semantic Structure

<ContentDetail composition={composition}>
<ContentDetail.Header>
<ContentDetail.Title accessibilityRole="header" accessibilityLevel={1} />
</ContentDetail.Header>

<ContentDetail.Body>
<ContentDetail.RichContent accessibilityRole="article" />
</ContentDetail.Body>
</ContentDetail>

Screen Reader Support

<ContentDetail.Tags
accessibilityLabel="Article tags"
accessibilityHint="Tap tags to filter related content"
onTagPress={(tag) => {
Accessibility.announceForAccessibility(`Filtering by ${tag.display}`);
filterByTag(tag);
}}
/>

Performance Considerations

  • HTML Rendering: Optimized HTML parsing and rendering with caching
  • Image Optimization: Automatic image optimization through OptimizedImage
  • Context Memoization: Efficient context value memoization
  • Scroll Performance: Optimized scroll view with proper content sizing
  • Memory Management: Automatic cleanup of resources and event listeners

Error Handling

Missing Content

<ContentDetail composition={composition}>
<ContentDetail.Header>
<ContentDetail.Title />
</ContentDetail.Header>

<ContentDetail.Body>
<ErrorBoundary
fallback={<Text>Content could not be loaded. Please try again.</Text>}
>
<ContentDetail.RichContent />
</ErrorBoundary>
</ContentDetail.Body>
</ContentDetail>

Network Issues

const handleResourcePress = async (resourceLink: string) => {
try {
await WebBrowser.openBrowserAsync(resourceLink);
} catch (error) {
Alert.alert(
"Unable to open link",
"Please check your internet connection and try again.",
);
}
};

<ContentDetail.Resource onPress={handleResourcePress} />;

Testing

Unit Testing

import { render, screen } from "@testing-library/react-native";
import { ContentDetail } from "../content-detail";

const mockComposition = {
title: "Test Article",
section: [
{
text: { div: "<p>Test content</p>" },
},
],
};

describe("ContentDetail", () => {
it("renders title from composition", () => {
render(
<ContentDetail composition={mockComposition}>
<ContentDetail.Title />
</ContentDetail>,
);

expect(screen.getByText("Test Article")).toBeTruthy();
});

it("renders rich content from composition", () => {
render(
<ContentDetail composition={mockComposition}>
<ContentDetail.RichContent />
</ContentDetail>,
);

expect(screen.getByText("Test content")).toBeTruthy();
});
});

Dependencies

  • react-native-render-html: HTML content rendering
  • @medplum/fhirtypes: FHIR TypeScript definitions
  • date-fns: Date formatting utilities
  • expo-web-browser: External link handling
  • react-i18next: Internationalization support