Skip to content

Commit 24c841a

Browse files
authored
Merge pull request #151 from episerver/bugfix/CMS-46488-richtext-fallback
CMS-46488 Fix/Improve Richtext fallback Issues
2 parents a3790bf + ddd6efa commit 24c841a

File tree

7 files changed

+206
-200
lines changed

7 files changed

+206
-200
lines changed

docs/6-rendering-react.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export default function Article({ opti }: Props) {
6666
}
6767
```
6868

69+
> [!NOTE]
70+
> For comprehensive documentation on the RichText component including all props, advanced customization options, fallback handling, and TypeScript support, see the [RichText Component Reference](./6.1-richtext-component-react.md).
71+
6972
The entire file should look like this:
7073

7174
```tsx

docs/7-richtext-component-react.md renamed to docs/6.1-richtext-component-react.md

Lines changed: 130 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -201,119 +201,6 @@ const CodeExample = ({ content }) => (
201201
);
202202
```
203203

204-
### `elementFallback`
205-
206-
**Type:** `string`
207-
**Default:** `'div'`
208-
209-
The HTML element to use when an unknown or unsupported element type is encountered. This provides graceful degradation for content with elements not explicitly handled.
210-
211-
#### Example: Custom Element Fallback
212-
213-
```tsx
214-
// Use span for unknown elements (better for inline contexts)
215-
<RichText
216-
content={content.body?.json}
217-
elementFallback="span"
218-
/>
219-
220-
// Use section for semantic grouping
221-
<RichText
222-
content={content.body?.json}
223-
elementFallback="section"
224-
/>
225-
```
226-
227-
#### Advanced Fallback with Custom Component
228-
229-
```tsx
230-
// Create a custom fallback that logs unknown elements
231-
const CustomFallbackElement = ({ children, element }: ElementProps) => {
232-
console.warn('Unknown element type:', element.type);
233-
234-
return (
235-
<div
236-
className="unknown-element"
237-
data-type={element.type}
238-
style={{ border: '1px dashed #ccc', padding: '8px' }}
239-
>
240-
<small>Unknown element: {element.type}</small>
241-
{children}
242-
</div>
243-
);
244-
};
245-
246-
<RichText
247-
content={content.body?.json}
248-
elements={{
249-
// Handle known elements...
250-
'unknown-element': CustomFallbackElement,
251-
}}
252-
/>;
253-
```
254-
255-
### `leafFallback`
256-
257-
**Type:** `string`
258-
**Default:** `'span'`
259-
260-
The HTML element to use when an unknown text formatting mark is encountered.
261-
262-
#### Example: Custom Leaf Fallback
263-
264-
```tsx
265-
// Use em for unknown marks
266-
<RichText
267-
content={content.body?.json}
268-
leafFallback="em"
269-
/>
270-
271-
// Use mark element for highlighting unknown formatting
272-
<RichText
273-
content={content.body?.json}
274-
leafFallback="mark"
275-
/>
276-
```
277-
278-
#### Advanced Fallback with Styling
279-
280-
```tsx
281-
// Visual indication of unknown formatting
282-
const CustomFallbackLeaf = ({ children, leaf }: LeafProps) => {
283-
const unknownMarks = Object.keys(leaf).filter(
284-
(key) =>
285-
![
286-
'text',
287-
'bold',
288-
'italic',
289-
'underline',
290-
'code',
291-
'strikethrough',
292-
].includes(key)
293-
);
294-
295-
return (
296-
<span
297-
className="unknown-formatting"
298-
title={`Unknown formatting: ${unknownMarks.join(', ')}`}
299-
style={{ backgroundColor: 'yellow', padding: '0 2px' }}
300-
>
301-
{children}
302-
</span>
303-
);
304-
};
305-
306-
// Apply to all unknown marks
307-
<RichText
308-
content={content.body?.json}
309-
leafs={{
310-
// Known formatting...
311-
bold: ({ children }) => <strong>{children}</strong>,
312-
// This would handle any unknown marks
313-
}}
314-
/>;
315-
```
316-
317204
## Complete Example
318205

319206
```tsx
@@ -377,8 +264,6 @@ export default function Article({ content }) {
377264
code: CustomCode,
378265
}}
379266
decodeHtmlEntities={true}
380-
elementFallback="div"
381-
leafFallback="span"
382267
/>
383268
</article>
384269
);
@@ -412,12 +297,140 @@ const leafs: LeafMap = {
412297
};
413298
```
414299

300+
## Fallback Handling for Unknown Elements
301+
302+
The RichText component uses a simple and safe fallback strategy for unknown elements and text marks.
303+
304+
### Default Behavior
305+
306+
**All unknown elements and text marks render as `<span>` elements.**
307+
308+
This ensures:
309+
310+
- Valid HTML in any context (inline or block)
311+
- No hydration errors in React
312+
- Safe rendering without breaking layout
313+
- Can be styled via CSS if needed
314+
315+
### How It Works
316+
317+
- **Known Elements**: Use their default HTML tags (e.g., `heading-one``<h1>`, `paragraph``<p>`)
318+
- **Unknown Elements**: Automatically become `<span>` tags
319+
- **Known Text Marks**: Use their default tags (e.g., `bold``<strong>`, `italic``<em>`)
320+
- **Unknown Text Marks**: Automatically become `<span>` tags
321+
322+
### Built-in Element Support
323+
324+
The component includes extensive built-in support for common HTML elements:
325+
326+
**Text semantics:** `span`, `mark`, `strong`, `em`, `u`, `s`, `i`, `b`, `small`, `sub`, `sup`, `ins`, `del`, `kbd`, `abbr`, `cite`, `dfn`, `q`, `time`, `data`, `bdo`, `bdi`, `var`, `samp`
327+
328+
**Interactive:** `a`, `button`, `label`
329+
330+
**Media:** `img`, `svg`, `canvas`
331+
332+
**Forms:** `input`, `select`, `textarea`
333+
334+
**Other:** `br`, `wbr`
335+
336+
### Custom Element Handling
337+
338+
If you encounter unknown HTML tags or elements not supported by the SDK, you can override them using the `elements` or `leafs` props to render custom React components.
339+
340+
> [!NOTE]
341+
> Unknown elements and text marks are typically introduced when rich text content is created through the **Integration API (REST API)** rather than the CMS editor. When using the Integration API, developers can insert custom HTML elements or formatting that may not be recognized by the SDK's default element mappings. Additionally, some element attributes may not be fully supported when content is created via the Integration API, as the TinyMCE editor processing that normalizes and validates these attributes is bypassed.
342+
343+
```tsx
344+
import {
345+
RichText,
346+
ElementProps,
347+
LeafProps,
348+
} from '@optimizely/cms-sdk/react/richText';
349+
350+
// Custom component for unknown block elements
351+
const UnknownElement = ({ children, element }: ElementProps) => (
352+
<div className="unknown-element" data-type={element.type}>
353+
{children}
354+
</div>
355+
);
356+
357+
// Custom component for unknown text marks
358+
const UnknownLeaf = ({ children, leaf }: LeafProps) => (
359+
<span className="unknown-leaf" data-marks={Object.keys(leaf).join(',')}>
360+
{children}
361+
</span>
362+
);
363+
364+
// Custom component for a specific custom element
365+
const CustomElement = ({ children, element }: ElementProps) => (
366+
<div className="custom-element">{children}</div>
367+
);
368+
369+
function Article({ content }) {
370+
return (
371+
<RichText
372+
content={content.body?.json}
373+
elements={{
374+
'unknown-element': UnknownElement, // Handle specific unknown element type
375+
'custom-element': CustomElement, // Your custom CMS element
376+
}}
377+
leafs={{
378+
'unknown-leaf': UnknownLeaf, // Handle specific unknown text mark
379+
highlight: ({ children }) => (
380+
<mark className="highlight">{children}</mark>
381+
),
382+
}}
383+
/>
384+
);
385+
}
386+
```
387+
388+
#### Example: Handling Custom CMS Elements
389+
390+
```tsx
391+
// Custom video element not supported by default
392+
const VideoElement = ({ children, element }: ElementProps) => {
393+
const videoData = element as { videoUrl?: string; autoplay?: boolean };
394+
395+
return (
396+
<div className="video-container">
397+
<video src={videoData.videoUrl} autoPlay={videoData.autoplay} controls>
398+
{children}
399+
</video>
400+
</div>
401+
);
402+
};
403+
404+
// Custom callout box element
405+
const CalloutElement = ({ children, element }: ElementProps) => {
406+
const calloutData = element as { variant?: string };
407+
408+
return (
409+
<div className={`callout callout-${calloutData.variant || 'info'}`}>
410+
{children}
411+
</div>
412+
);
413+
};
414+
415+
function Article({ content }) {
416+
return (
417+
<RichText
418+
content={content.body?.json}
419+
elements={{
420+
'video-embed': VideoElement,
421+
'callout-box': CalloutElement,
422+
}}
423+
/>
424+
);
425+
}
426+
```
427+
415428
## Best Practices
416429

417430
1. **Performance**: Only override elements/leafs you need to customize
418431
2. **Accessibility**: Maintain semantic HTML structure in custom components
419-
3. **Fallbacks**: Always provide sensible fallback values for unknown content
420-
4. **Type Safety**: Use TypeScript interfaces for better development experience
432+
3. **Type Safety**: Use TypeScript interfaces for better development experience
433+
4. **Unknown Elements**: The default `<span>` fallback handles unknown elements safely - no configuration needed
421434

422435
## Common Use Cases
423436

packages/optimizely-cms-sdk/src/components/richText/base.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ import {
99
/**
1010
* Configuration for rich text renderers
1111
*/
12-
export interface BaseRendererConfig extends RendererConfig {
13-
elementFallback?: string;
14-
leafFallback?: string;
15-
}
12+
export interface BaseRendererConfig extends RendererConfig {}
1613

1714
/**
1815
* Base class for rich text renderers that provides common functionality
@@ -28,8 +25,6 @@ export abstract class BaseRichTextRenderer<
2825
constructor(config: Partial<BaseRendererConfig> = {}) {
2926
this.config = {
3027
decodeHtmlEntities: true,
31-
elementFallback: 'div',
32-
leafFallback: 'span',
3328
...config,
3429
};
3530
}

0 commit comments

Comments
 (0)