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:
| Prop | Type | Default | Description |
|---|---|---|---|
composition | Composition | undefined | FHIR Composition resource for content data |
children | ReactNode | required | Content detail sub-components |
contentContainerStyle | ViewStyle | undefined | Additional styles for scroll content container |
ContentDetail.Title
Extends TextProps from react-native-paper:
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | undefined | Custom 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:
| Prop | Type | Default | Description |
|---|---|---|---|
content | string | undefined | Custom HTML content (overrides composition content) |
ContentDetail.Tags
Extends ViewProps:
| Prop | Type | Default | Description |
|---|---|---|---|
tags | Coding[] | undefined | Custom tags array (overrides composition tags) |
onTagPress | (tag: Coding) => void | undefined | Handler for tag press interactions |
chipProps | Partial<ChipProps> | undefined | Additional props for tag chips |
ContentDetail.Metadata Components
All metadata components extend TextProps and support:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | undefined | Custom 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 contenttheme.colors.blue[500]- Resource link colortheme.spacing(4)- Container padding and gaptheme.spacing(3)- Section gapstheme.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 definitionsdate-fns: Date formatting utilitiesexpo-web-browser: External link handlingreact-i18next: Internationalization support
Related Components
ContentCard: Content preview card componentContentList: List components for organizing contentOptimizedImage: Image optimization and display component