Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with bullet RTL #286

Closed
mohamedshuaau opened this issue Aug 13, 2019 · 8 comments
Closed

Issue with bullet RTL #286

mohamedshuaau opened this issue Aug 13, 2019 · 8 comments

Comments

@mohamedshuaau
Copy link

I first off want to say, great package! I ran in to a bit of a problem and I am not sure how to resolve it.

I have an RTL layout with an RTL language. As React-Native auto aligns RTL languages to the right, my parsed HTML is aligned to the right. But the bullets seems to stay on the left. I have tried alignText: 'right' on the baseFontStyle with no luck.

Here is the screenshot of what I am getting.

https://ibb.co/NZyCkjM

Thanks

@ncryer
Copy link

ncryer commented Dec 17, 2019

I just had this problem, my solution is below. The issue stems from the way the ul renderer is implemented, so overriding the ul tag with a custom renderer will likely fix your issue (it did mine). Fundamentally, the problem is that the standard ul renderer parses each list item into a prefix (bullet or number) and a child (text), and rendering order is hard-coded.

For the example below (lifted lazily from my own code), I've wrapped the HTML element in a custom component and passed in a boolean variable this.props.RTL to inform me of the text direction. Then I make a custom renderer to conditionally change the rendering order, and do a style tweak here and there:

import {_constructStyles} from 'react-native-render-html/src/HTMLStyles'
import HTML from 'react-native-render-html'
import React, { Component } from 'react'
import { Text, Linking, View } from 'react-native'

class HTMLRenderer extends Component {
    render() {
        return (
            <HTML 
            html={this.props.html}

            renderers={{
                ul: (htmlAttribs, children, convertedCSSStyles, passProps) => {
                    const style = _constructStyles({
                        tagName: 'ul',
                        htmlAttribs,
                        passProps,
                        styleSet: 'VIEW'
                    });
                    const { allowFontScaling, rawChildren, nodeIndex, key, baseFontStyle, listsPrefixesRenderers } = passProps;
                    const baseFontSize = baseFontStyle.fontSize || 14;
                
                    children = children && children.map((child, index) => {
                        const rawChild = rawChildren[index];
                        let prefix = false;
                        const rendererArgs = [
                            htmlAttribs,
                            children,
                            convertedCSSStyles,
                            {
                                ...passProps,
                                index
                            }
                        ];
                
                        if (rawChild) {
                            if (rawChild.parentTag === 'ul' && rawChild.tagName === 'li') {
                                prefix = listsPrefixesRenderers && listsPrefixesRenderers.ul ? listsPrefixesRenderers.ul(...rendererArgs) : (
                                    <View style={{
                                        marginRight: 10,
                                        width: baseFontSize / 2.8,
                                        height: baseFontSize / 2.8,
                                        marginTop: baseFontSize / 2,
                                        borderRadius: baseFontSize / 2.8,
                                        backgroundColor: 'black',
                                        marginLeft: this.props.RTL ? 10 : 0
                                    }} />
                                );
                            } else if (rawChild.parentTag === 'ol' && rawChild.tagName === 'li') {
                                prefix = listsPrefixesRenderers && listsPrefixesRenderers.ol ? listsPrefixesRenderers.ol(...rendererArgs) : (
                                    <Text allowFontScaling={allowFontScaling} style={{ 
                                        marginRight: 5, 
                                        fontSize: baseFontSize, 
                                        marginLeft: this.props.RTL ? 10 : 0 }}>{ index + 1 })</Text>
                                );
                            }
                        }
                        return (
                            this.props.RTL ? (
                                <View key={`list-${nodeIndex}-${index}-${key}`} style={{ flexDirection: 'row', marginBottom: 10 }}>
                                    <View style={{ flex: 1 }}>{ child }</View>
                                    { prefix }
                                </View>
                            ) : (
                                <View key={`list-${nodeIndex}-${index}-${key}`} style={{ flexDirection: 'row', marginBottom: 10 }}>
                                    { prefix }
                                    <View style={{ flex: 1 }}>{ child }</View>
                                </View>
                            )
                            
                        );
                    });
                    return (
                        <View style={style} key={key}>
                            { children }
                        </View>
                    );
                }
            }}
            ></HTML>
        )
    }
}

I hope that makes sense. Most of the code is directly from HTMLRenderers.js, ctrl+f for this.props.RTL ? to see where the conditional rendering comes in.

My heartfelt thanks to the author of this amazing library, it's really great.

@jsamr
Copy link
Collaborator

jsamr commented Apr 2, 2021

I've been exploring this issue a little bit for the foundry release (see #430), below are my findings.

I. RTL in the Web

The Web standards offer basically two ways to handle RTL typography.

  1. The HTML element dir attribute, with values ltr, rtl and auto (determined by text content).
  2. The inheritable CSS property direction, with values ltr and rtl. When both dir attribute and direction property are defined, the CSS property takes precedence.

When direction is set to rtl, the order of elements is the horizontal axis
are reversed under those conditions:

  • In a flex formatting context, the container providing this context has a flexDirection set to row;
  • In an inline formatting context, elements are text nodes or have an outer display type set to inline-block.
  • Elements are children of an element which has an inner display type set to table-row.

In CSS, the box model of an element can be direction and writing-mode dependent thanks to the below shorthand properties and their longhand equivalents:

  • margin-inline,
  • padding-inline
  • border-inline
  • border-start-*-radius
  • border-end-*-radius
  • overflow-inline ...

The set of all these properties is referred to as CSS logical properties.

Back to lists now... This is how the style of UL elements is defined in Firefox stylesheet:

ul,
menu,
dir {
  display: block;
  list-style-type: disc;
  margin-block-start: 1em;
  margin-block-end: 1em;
  padding-inline-start: 40px;
}

Notice the padding-inline-start which guarantees that the marker (disc) will have a direction-dependent padding.

II. RTL in React Native

Unfortunately, RTL support in React Native is yet limited. As of React Native 0.64.0, I know three features:

  1. I18nManager.isRTL and I18nManager.forceRTL(/*bool*/). This export is not documented! I visited the source code and I found no comments. I found out about this feature in this article. Using forceRTL requires you to restart the application for the change to take effect. When active, it reverses the order of elements in a container with flexDirection: "row" set. Other implications of this setting must be investigated. If you had an opportunity to test, please post your findings below. Especially, are left and right style props reversed? Such as paddingLeft, marginLeft, borderTopLeftRadius ... etc. Actually, I found out that the Yoga engine is RTL friendly (it only has start / end for horizontal directional rules), and React Native maps left → start and right → end, which is very confusing and prevent us from having "true left / right" directions support. See Right-to-Left Layout Support For React Native Apps.
  2. Layout direction style. Promising, but only works on iOS. On Android, the direction of text is automatically detected depending on content. This is equivalent to HTML elements dir attribute set to auto.
  3. Partial support for logical properties (on the inline axis, there is no support for vertical writing modes): Layout start, end, borderStart, borderEnd, marginStart, marginEnd, paddingStart, paddingEnd properties, which only applies to iOS when direction is set to 'rtl'. This is so confusing because as seen in 1., left and right directional rules are mapped to start / end in Yoga...

III. Foreseeable solutions

Short term: configurable lists layout

A new prop would force lists layouts to display RTL. When this prop is set to true, list item renderers will use flexDirection: "row-reversed" when I18nManager.isRTL value is false, and flexDirection: "row" otherwise. Left margins might be replaced with right margins (depending on our findings in II.1).

Long term: featured support in @native-html/css-parser

RTL support will be embedded in the CSS parser. The latter will be in charge of translating both CSS logical properties and react native logical properties to their physical equivalent depending on directional context and the current I18nManager.isRTL state. As seen in II.1, left / right will have to be reversed in RTL (so left will become right and vice-versa) to preserve the CSS semantics. So if one inline style specifies left: 10 and the app is in RTL mode, the rule will be translated to right: 10, which will render as a physical left spacing , that's quite confusing 😫!

Such support is a substantial endeavor and won't be available before version 7.0 of this library.

@jsamr
Copy link
Collaborator

jsamr commented Apr 13, 2021

Good news folks, I have made great progress regarding lists in general, and RTL in particular. I've just released a standalone component, @jsamr/react-native-li which can handle 47 presets including RTL languages (Arabic, Hebrew, Farsi) and has premium RTL support. I'll integrate this component in the foundry release in the upcoming days.

Lower Latin

show
import React from 'react';
import { ScrollView, StyleSheet, Text } from 'react-native';
import lowerLatin from '@jsamr/counter-style/presets/lowerLatin';
import MarkedList from '@jsamr/react-native-li';

export default function App() {
  return (
    <ScrollView style={{ flexGrow: 1 }}>
      <MarkedList counterRenderer={lowerLatin}>
        {[...Array(100).keys()].map((index) => (
          <Text key={index} style={{ flexShrink: 1 }}>
            The World Wide Web Consortium (W3C)
            develops international standards
            for the web and HTML, CSS, and more.
          </Text>
        ))}
      </MarkedList>
    </ScrollView>
  );
}

Disc

show
import React from 'react';
import { ScrollView, StyleSheet, Text } from 'react-native';
import disc from '@jsamr/counter-style/presets/disc';
import MarkedList from '@jsamr/react-native-li';

export default function App() {
  return (
    <ScrollView style={{ flexGrow: 1 }}>
      <MarkedList counterRenderer={disc}>
        {[...Array(100).keys()].map((index) => (
          <Text key={index} style={{ flexShrink: 1 }}>
            The World Wide Web Consortium (W3C)
            develops international standards
            for the web and HTML, CSS, and more.
          </Text>
        ))}
      </MarkedList>
    </ScrollView>
  );
}

Arabic + RTL

show
import React from 'react';
import { ScrollView, StyleSheet, Text } from 'react-native';
import arabicIndic from '@jsamr/counter-style/presets/arabicIndic';
import MarkedList from '@jsamr/react-native-li';

export default function App() {
  return (
    <ScrollView style={{ flexGrow: 1 }}>
      <MarkedList
        counterRenderer={arabicIndic}
        rtlLineReversed
        rtlMarkerReversed>
        {[...Array(100).keys()].map((index) => (
          <Text key={index} style={{ flexShrink: 1 }}>
            يقوم اتحاد شبكة الويب العالمية (W3C)
            بتطوير معايير دولية للويب و HTML و
            CSS وغير ذلك الكثير.
          </Text>
        ))}
      </MarkedList>
    </ScrollView>
  );
}

Disc + RTL

show
import React from 'react';
import { ScrollView, StyleSheet, Text } from 'react-native';
import disc from '@jsamr/counter-style/presets/disc';
import MarkedList from '@jsamr/react-native-li';

export default function App() {
  return (
    <ScrollView style={{ flexGrow: 1 }}>
      <MarkedList
        counterRenderer={disc}
        rtlLineReversed
        rtlMarkerReversed>
        {[...Array(100).keys()].map((index) => (
          <Text key={index} style={{ flexShrink: 1 }}>
            يقوم اتحاد شبكة الويب العالمية (W3C)
            بتطوير معايير دولية للويب و HTML و
            CSS وغير ذلك الكثير.
          </Text>
        ))}
      </MarkedList>
    </ScrollView>
  );
}

@jsamr
Copy link
Collaborator

jsamr commented Apr 17, 2021

🚀 Experimental support for ol and ul RTL mode has been shipped in the last foundry pre-release (6.0.0-alpha.23)
See #430 for install instructions.

import React from 'react';
import { ScrollView } from 'react-native';
import RenderHTML from 'react-native-render-html';

const html = `
<ul dir="rtl">
  <li>يقوم اتحاد شبكة الويب العالمية (W3C) 
  بتطوير معايير دولية للويب و HTML و
  CSS وغير ذلك الكثير.</li>
  <li>يقوم اتحاد شبكة الويب العالمية (W3C) 
  بتطوير معايير دولية للويب و HTML و
  CSS وغير ذلك الكثير.</li>
  <li>يقوم اتحاد شبكة الويب العالمية (W3C) 
  بتطوير معايير دولية للويب و HTML و
  CSS وغير ذلك الكثير.</li>
  <li>يقوم اتحاد شبكة الويب العالمية (W3C) 
  بتطوير معايير دولية للويب و HTML و
  CSS وغير ذلك الكثير.</li>
</ul>
`;

const renderersProps = {
  // Experimental RTL support is only available for list elements.
  ol: { enableExperimentalRtl: true },
  ul: { enableExperimentalRtl: true }
};

const tagsStyles = {
  // We need to reverse default styles for RTL
  ul: { paddingLeft: 0, paddingRight: 30 },
  ol: { paddingLeft: 0, paddingRight: 30 }
};

export default function App() {
  return (
    <ScrollView style={{ flexGrow: 1 }}>
      <RenderHTML
        source={{ html }}
        tagsStyles={tagsStyles}
        renderersProps={renderersProps}
      />
    </ScrollView>
  );
}

Remarks:

  • Notice the required dir="rtl" attribute. CSS rule "direction: rtl" will also work.
  • Default UA styles set padding left for ol and ul.
SHOW RESULT

Screenshot_1618695613

Now, just to show you the new capabilities of this version, let's implement arabic-indic support:

import React from 'react';
import { ScrollView } from 'react-native';
import RenderHTML from 'react-native-render-html';
import arabicIndic from '@jsamr/counter-style/presets/arabicIndic';

const html = `
<ol dir="rtl" style="list-style-type: arabic-indic;">
  <li>يقوم اتحاد شبكة الويب العالمية (W3C) 
  بتطوير معايير دولية للويب و HTML و
  CSS وغير ذلك الكثير.</li>
  <li>يقوم اتحاد شبكة الويب العالمية (W3C) 
  بتطوير معايير دولية للويب و HTML و
  CSS وغير ذلك الكثير.</li>
  <li>يقوم اتحاد شبكة الويب العالمية (W3C) 
  بتطوير معايير دولية للويب و HTML و
  CSS وغير ذلك الكثير.</li>
  <li>يقوم اتحاد شبكة الويب العالمية (W3C) 
  بتطوير معايير دولية للويب و HTML و
  CSS وغير ذلك الكثير.</li>
</ol>
`;

const renderersProps = {
  // Experimental RTL support is only available for list elements.
  ol: { enableExperimentalRtl: true },
  ul: { enableExperimentalRtl: true }
};

const tagsStyles = {
  // We need to reverse default styles for RTL
  ul: { paddingLeft: 0, paddingRight: 30 },
  ol: { paddingLeft: 0, paddingRight: 30 }
};

const customListStyleSpecs = {
  'arabic-indic': {
    type: 'textual',
    counterStyleRenderer: arabicIndic
  }
};

export default function App() {
  return (
    <ScrollView style={{ flexGrow: 1 }}>
      <RenderHTML
        source={{ html }}
        tagsStyles={tagsStyles}
        renderersProps={renderersProps}
        customListStyleSpecs={customListStyleSpecs}
      />
    </ScrollView>
  );
}
SHOW RESULT

Screenshot_1618696285

Check @jsamr/counter-style library for more presets and guides to implement arbitrary counter styles

@jsamr
Copy link
Collaborator

jsamr commented Jun 8, 2021

Closing now since the stable beta which fixes this issue has just been released.

@jsamr jsamr closed this as completed Jun 8, 2021
@Stevemoretz
Copy link

setting textAlign: "left" on base style fixes other kind of problems with other tags e.g. span, ...

@jsamr
Copy link
Collaborator

jsamr commented Jul 20, 2021

@Stevemoretz would be very helpful if you shared an example of those issues ; perhaps on Discord?

@idanlevi1
Copy link

idanlevi1 commented Jan 14, 2023

Try to add (Work only in IOS):

body: {
        writingDirection: 'rtl',
      }

in additionalProps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants