Skip to content

Commit ef08cfb

Browse files
authored
Merge pull request #157 from episerver/bugfix/CMS-46481-fix-attributes-parsing
Bugfix/cms 46481 fix attributes parsing
2 parents 1ef7593 + b79c778 commit ef08cfb

File tree

9 files changed

+1050
-28
lines changed

9 files changed

+1050
-28
lines changed

docs/6-rendering-react.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export default function Article({ opti }: Props) {
6767
```
6868

6969
> [!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).
70+
> For complete 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).
7171
7272
The entire file should look like this:
7373

docs/6.1-richtext-component-react.md

Lines changed: 336 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ function Article({ content }) {
2626
>
2727
> When rich text content is created through Optimizely's Integration API (REST API) rather than the CMS editor interface, certain features may not function correctly:
2828
>
29-
> - **Inline styles** will not be processed or rendered
30-
> - **HTML validation** is bypassed, which can result in malformed or invalid HTML that causes rendering issues
31-
> - **Advanced formatting** that relies on TinyMCE editor processing may be missing
32-
> - **Custom attributes or props** might not be mapped properly from raw HTML to React components
33-
> - **Security sanitization** performed by the editor may not occur
29+
> - **Inline styles** - Some inline CSS properties might not work as expected. See [Attribute and CSS Property Support](#attribute-and-css-property-support) for details on supported CSS properties.
30+
> - **HTML validation** is bypassed, which can result in malformed or invalid HTML that causes rendering issues.
31+
> - **Advanced formatting** that relies on TinyMCE editor processing may be missing.
32+
> - **Custom attributes or props** might not be mapped properly from raw HTML to React components.
33+
> - **Security sanitization** performed by the editor may not occur.
3434
>
3535
> For full feature compatibility and optimal rendering, create rich text content using the CMS's built-in TinyMCE editor interface.
3636
@@ -425,11 +425,339 @@ function Article({ content }) {
425425
}
426426
```
427427

428+
## Attribute and CSS Property Support
429+
430+
Under the hood, the RichText component performs sophisticated attribute and CSS property processing to ensure React compatibility. Here's how it handles the technical challenges of converting CMS content to proper React elements.
431+
432+
> **🔧 Technical Deep Dive:** The component normalizes HTML attributes to camelCase React props and moves CSS properties to React's `style` object, preventing warnings and ensuring proper rendering.
433+
434+
### HTML Attributes (Converted to React Props)
435+
436+
The component automatically normalizes HTML attributes to React-compatible prop names. This conversion happens through a predefined mapping table that handles common naming conflicts between HTML and React.
437+
438+
> **⚠️ React Compatibility:** React requires camelCase prop names for DOM attributes, but HTML uses kebab-case. Our mapping system handles this conversion automatically to prevent console warnings.
439+
440+
#### **Core Required Mappings**
441+
442+
**Reserved Keywords (Must be mapped):**
443+
444+
- `class``className`
445+
- `for``htmlFor`
446+
447+
**Table Attributes:**
448+
449+
- `colspan``colSpan`
450+
- `rowspan``rowSpan`
451+
- `cellpadding``cellPadding`
452+
- `cellspacing``cellSpacing`
453+
454+
#### **Form & Input Attributes**
455+
456+
- `tabindex`, `tab-index``tabIndex`
457+
- `readonly``readOnly`
458+
- `maxlength``maxLength`
459+
- `minlength``minLength`
460+
- `autocomplete``autoComplete`
461+
- `autofocus``autoFocus`
462+
- `autoplay``autoPlay`
463+
- `contenteditable`, `content-editable``contentEditable`
464+
- `spellcheck``spellCheck`
465+
- `novalidate``noValidate`
466+
467+
#### **Media Attributes**
468+
469+
- `crossorigin``crossOrigin`
470+
- `usemap``useMap`
471+
- `allowfullscreen``allowFullScreen`
472+
- `frameborder``frameBorder`
473+
- `playsinline``playsInline`
474+
- `srcset``srcSet`
475+
- `srcdoc``srcDoc`
476+
- `srclang``srcLang`
477+
478+
#### **Meta & Form Attributes**
479+
480+
- `accept-charset``acceptCharset`
481+
- `http-equiv``httpEquiv`
482+
- `charset``charSet`
483+
- `datetime``dateTime`
484+
- `hreflang``hrefLang`
485+
- `accesskey``accessKey`
486+
- `autocapitalize``autoCapitalize`
487+
- `referrerpolicy``referrerPolicy`
488+
- `formaction``formAction`
489+
- `formenctype``formEnctype`
490+
- `formmethod``formMethod`
491+
- `formnovalidate``formNoValidate`
492+
- `formtarget``formTarget`
493+
- `enctype``encType`
494+
495+
#### **Attributes That Work As-Is**
496+
497+
> **🚀 Performance Optimization:** Many HTML attributes work directly in React without conversion, so they're not included in our mapping table for better performance.
498+
499+
These attributes work directly in React:
500+
501+
- **Basic**: `id`, `name`, `value`, `type`, `href`, `src`, `alt`, `title`
502+
- **Form**: `disabled`, `checked`, `selected`, `multiple`, `required`, `placeholder`, `pattern`, `min`, `max`, `step`
503+
- **Interactive**: `draggable`, `hidden`, `lang`, `dir`, `role`
504+
- **Media**: `width`, `height`, `preload`, `loop`, `muted`, `controls`
505+
- **Security**: `nonce`, `sandbox`, `download`
506+
507+
#### **Special Attribute Handling**
508+
509+
**ARIA Attributes (Preserved as-is):**
510+
All ARIA attributes remain in kebab-case as React requires:
511+
512+
- `aria-label`, `aria-labelledby`, `aria-describedby`
513+
- `aria-hidden`, `aria-expanded`, `aria-*` (all ARIA attributes)
514+
515+
**Data Attributes (Preserved as-is):**
516+
517+
- `data-*` (all data attributes are preserved in kebab-case)
518+
519+
### CSS Properties (Moved to Style Object)
520+
521+
CSS properties are automatically detected using a curated set of known CSS property names and moved to React's `style` object with proper camelCase conversion.
522+
523+
> **🎨 Style Processing:** React requires CSS properties to be in a style object with camelCase keys. Properties like `background-color` become `style.backgroundColor`. This system prevents invalid DOM property warnings.
524+
525+
#### **Layout & Sizing**
526+
527+
- `min-width``style.minWidth`
528+
- `max-width``style.maxWidth`
529+
- `min-height``style.minHeight`
530+
- `max-height``style.maxHeight`
531+
532+
#### **Spacing**
533+
534+
- `margin`, `margin-top`, `margin-right`, `margin-bottom`, `margin-left`
535+
- `padding`, `padding-top`, `padding-right`, `padding-bottom`, `padding-left`
536+
537+
#### **Typography**
538+
539+
- `font`, `font-family`, `font-size`, `font-weight`, `font-style`, `font-variant`
540+
- `line-height`, `letter-spacing`, `word-spacing`
541+
- `text-align`, `text-decoration`, `text-transform`, `text-indent`, `text-shadow`
542+
- `vertical-align`
543+
544+
#### **Colors & Backgrounds**
545+
546+
- `color`
547+
- `background`, `background-color`, `background-image`, `background-repeat`
548+
- `background-position`, `background-size`, `background-attachment`
549+
- `background-clip`, `background-origin`
550+
551+
#### **Borders**
552+
553+
- `border-width`, `border-style`, `border-color`, `border-radius`
554+
- `border-top`, `border-right`, `border-bottom`, `border-left`
555+
- `border-*-width`, `border-*-style`, `border-*-color` (all directional variants)
556+
- `border-*-radius` (all corner variants)
557+
558+
#### **Positioning**
559+
560+
- `position`, `top`, `right`, `bottom`, `left`, `z-index`
561+
- `float`, `clear`
562+
563+
#### **Display & Visibility**
564+
565+
- `display`, `visibility`, `opacity`
566+
- `overflow`, `overflow-x`, `overflow-y`
567+
- `clip`, `clip-path`
568+
569+
#### **Flexbox**
570+
571+
- `flex`, `flex-direction`, `flex-wrap`, `flex-flow`
572+
- `justify-content`, `align-items`, `align-content`, `align-self`
573+
- `flex-grow`, `flex-shrink`, `flex-basis`
574+
575+
#### **Grid Layout**
576+
577+
- `grid`, `grid-template`, `grid-template-rows`, `grid-template-columns`
578+
- `grid-template-areas`, `grid-area`, `grid-row`, `grid-column`
579+
- `grid-gap`, `gap`, `row-gap`, `column-gap`
580+
581+
#### **Text & Content**
582+
583+
- `white-space`, `word-wrap`, `word-break`, `overflow-wrap`
584+
- `hyphens`, `text-overflow`, `direction`, `unicode-bidi`, `writing-mode`
585+
586+
#### **Visual Effects**
587+
588+
- `box-shadow`, `text-shadow`
589+
- `filter`, `backdrop-filter`
590+
- `transform`, `transform-origin`
591+
- `perspective`, `perspective-origin`
592+
593+
#### **Animation & Transitions**
594+
595+
- `transition`, `transition-property`, `transition-duration`
596+
- `transition-timing-function`, `transition-delay`
597+
- `animation`, `animation-name`, `animation-duration`
598+
- `animation-timing-function`, `animation-delay`
599+
- `animation-iteration-count`, `animation-direction`
600+
- `animation-fill-mode`, `animation-play-state`
601+
602+
#### **Interaction**
603+
604+
- `cursor`, `pointer-events`, `user-select`, `resize`
605+
606+
#### **Modern CSS**
607+
608+
- `aspect-ratio`, `object-fit`, `object-position`
609+
- `scroll-behavior`, `overscroll-behavior`
610+
- `scroll-snap-type`, `scroll-snap-align`
611+
- `scroll-margin`, `scroll-padding`
612+
613+
### Special Handling
614+
615+
#### **Dual-Purpose Properties**
616+
617+
Some properties exist in both HTML and CSS domains. The component uses context-aware logic to determine the correct handling:
618+
619+
- **`width`, `height`**: Treated as HTML attributes for tables and images, CSS properties for other elements
620+
- **`border`**: Treated as HTML attribute for tables, CSS property for other elements
621+
622+
#### **Table-Specific Attributes**
623+
624+
Table elements receive special handling for HTML attributes:
625+
626+
```tsx
627+
// These remain as HTML attributes for tables
628+
<table border="1" cellpadding="5" cellspacing="0" width="100%">
629+
<tr>
630+
<td colspan="2" rowspan="1">
631+
Content
632+
</td>
633+
</tr>
634+
</table>
635+
```
636+
637+
#### **Style Object Conversion**
638+
639+
The component performs real-time CSS property conversion using kebab-to-camelCase transformation:
640+
641+
```tsx
642+
// Input: Slate.js node with CSS properties as attributes
643+
{
644+
type: 'div',
645+
'font-size': '16px',
646+
'background-color': 'blue',
647+
'margin-top': '10px',
648+
children: [{ text: 'Styled content' }]
649+
}
650+
651+
// Output: React element with style object
652+
<div style={{
653+
fontSize: '16px',
654+
backgroundColor: 'blue',
655+
marginTop: '10px'
656+
}}>
657+
Styled content
658+
</div>
659+
```
660+
661+
### Unsupported CSS Properties
662+
663+
While our CSS property detection covers most real-world use cases, some advanced CSS features are intentionally excluded to maintain performance and simplicity:
664+
665+
#### **Print-Specific Properties**
666+
667+
- `page-break-before`, `page-break-after`, `page-break-inside`
668+
- `orphans`, `widows`
669+
670+
#### **Advanced Layout Features**
671+
672+
- Multi-column layout properties (`columns`, `column-count`, etc.)
673+
- CSS Masking properties (`mask`, `mask-image`, etc.)
674+
- Ruby text properties (`ruby-align`, `ruby-position`, etc.)
675+
676+
#### **Logical Properties**
677+
678+
- `margin-inline-start`, `margin-block-end`, etc.
679+
- `border-inline-start`, `border-block-end`, etc.
680+
681+
#### **CSS Custom Properties**
682+
683+
- CSS variables (`--custom-property`) are not automatically detected
684+
685+
### Extending Support
686+
687+
If you encounter unsupported properties in your CMS content, you can handle them by providing custom element components:
688+
689+
> **🔧 Extensibility:** The component is designed to be extended rather than modified. Use custom element components for application-specific handling of unsupported properties.
690+
691+
```tsx
692+
// Custom handling for unsupported properties
693+
const CustomDiv = ({ children, element, attributes }: ElementProps) => {
694+
const customStyle = {
695+
// Handle unsupported CSS properties manually
696+
'--custom-property': attributes['--custom-property'],
697+
columnCount: attributes['column-count'],
698+
};
699+
700+
return <div style={customStyle}>{children}</div>;
701+
};
702+
703+
<RichText
704+
content={content}
705+
elements={{
706+
div: CustomDiv,
707+
}}
708+
/>;
709+
```
710+
711+
### Technical Implementation Example
712+
713+
Here's how the attribute processing works in practice with a real Slate.js node structure:
714+
715+
```tsx
716+
// Rich text content in Slate.js format with mixed attributes
717+
const richTextContent = {
718+
type: 'richText',
719+
children: [
720+
{
721+
type: 'div',
722+
class: 'content-block',
723+
'data-testid': 'rich-content',
724+
'aria-label': 'Article content',
725+
width: '100%', // HTML attribute for some elements
726+
'font-size': '16px', // CSS property → style.fontSize
727+
'background-color': 'lightblue', // CSS property → style.backgroundColor
728+
'margin-top': '20px', // CSS property → style.marginTop
729+
border: '1px solid #ccc', // CSS property → style.border
730+
children: [
731+
{ text: 'This content has mixed HTML attributes and CSS properties' },
732+
],
733+
},
734+
],
735+
};
736+
737+
// Rendered as:
738+
<div
739+
className="content-block"
740+
data-testid="rich-content"
741+
aria-label="Article content"
742+
width="100%"
743+
style={{
744+
fontSize: '16px',
745+
backgroundColor: 'lightblue',
746+
marginTop: '20px',
747+
border: '1px solid #ccc',
748+
}}
749+
>
750+
This content has mixed HTML attributes and CSS properties
751+
</div>;
752+
```
753+
754+
This attribute and CSS property processing ensures that rich text content from Optimizely CMS renders correctly in React applications while maintaining proper HTML semantics and React compatibility.
755+
428756
## Best Practices
429757

430-
1. **Performance**: Only override elements/leafs you need to customize
431-
2. **Accessibility**: Maintain semantic HTML structure in custom components
432-
3. **Type Safety**: Use TypeScript interfaces for better development experience
758+
1. **Performance**: Only override elements/leafs you need to customize - the default implementations are optimized
759+
2. **Accessibility**: Maintain semantic HTML structure in custom components - screen readers depend on proper markup
760+
3. **Type Safety**: Use TypeScript interfaces for better development experience and catch errors at compile time
433761
4. **Unknown Elements**: The default `<span>` fallback handles unknown elements safely - no configuration needed
434762

435763
## Common Use Cases

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export type {
1111
BaseElementMap,
1212
BaseLeafMap,
1313
RichTextPropsBase,
14+
LinkElement,
15+
ImageElement,
16+
TableElement,
17+
TableCellElement,
1418
} from './renderer.js';
1519
export { isText, isElement } from './renderer.js';
1620
export type { RenderNode } from './renderer.js';

0 commit comments

Comments
 (0)