=
supportedProps.ref = setRef;
- const element = (
-
- {createElement(component, supportedProps)}
-
- );
+ const element = createElement(component, supportedProps, { writingDirection });
return hasTextAncestor ? (
element
diff --git a/packages/react-native-web/src/exports/Text/types.js b/packages/react-native-web/src/exports/Text/types.js
index ba401efc..9cac6f16 100644
--- a/packages/react-native-web/src/exports/Text/types.js
+++ b/packages/react-native-web/src/exports/Text/types.js
@@ -73,6 +73,7 @@ export type TextProps = {
| 'none'
| 'text',
dir?: 'auto' | 'ltr' | 'rtl',
+ lang?: string,
numberOfLines?: ?number,
onPress?: (e: any) => void,
selectable?: boolean,
diff --git a/packages/react-native-web/src/exports/TextInput/index.js b/packages/react-native-web/src/exports/TextInput/index.js
index e233e4d1..5b8f3af7 100644
--- a/packages/react-native-web/src/exports/TextInput/index.js
+++ b/packages/react-native-web/src/exports/TextInput/index.js
@@ -20,7 +20,7 @@ import useLayoutEffect from '../../modules/useLayoutEffect';
import useMergeRefs from '../../modules/useMergeRefs';
import usePlatformMethods from '../../modules/usePlatformMethods';
import useResponderEvents from '../../modules/useResponderEvents';
-import RTLContext from '../../modules/RTLContext';
+import { getLocaleDirection, useLocaleContext } from '../../modules/useLocale';
import StyleSheet from '../StyleSheet';
import TextInputState from '../../modules/TextInputState';
@@ -337,7 +337,7 @@ const TextInput: React.AbstractComponent<
onStartShouldSetResponder,
onStartShouldSetResponderCapture
});
- const isRTL = React.useContext(RTLContext);
+ const { direction: contextDirection } = useLocaleContext();
const supportedProps = pickProps(props);
supportedProps.autoCapitalize = autoCapitalize;
@@ -369,11 +369,13 @@ const TextInput: React.AbstractComponent<
supportedProps.ref = setRef;
- supportedProps.isRTL = isRTL || dir === 'rtl';
+ const langDirection = props.lang != null ? getLocaleDirection(props.lang) : null;
+ const componentDirection = props.dir || langDirection;
+ const writingDirection = componentDirection || contextDirection;
- const element = createElement(component, supportedProps);
+ const element = createElement(component, supportedProps, { writingDirection });
- return {element};
+ return element;
});
TextInput.displayName = 'TextInput';
diff --git a/packages/react-native-web/src/exports/View/__tests__/__snapshots__/index-test.js.snap b/packages/react-native-web/src/exports/View/__tests__/__snapshots__/index-test.js.snap
index 31160481..fc57e3e0 100644
--- a/packages/react-native-web/src/exports/View/__tests__/__snapshots__/index-test.js.snap
+++ b/packages/react-native-web/src/exports/View/__tests__/__snapshots__/index-test.js.snap
@@ -1,12 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`components/View allows "dir" to be overridden 1`] = `
-
-`;
-
exports[`components/View default 1`] = `
`;
+exports[`components/View prop "dir" value is "ltr" 1`] = `
+
+`;
+
+exports[`components/View prop "dir" value is "rtl" 1`] = `
+
+`;
+
exports[`components/View prop "href" href with accessibilityRole 1`] = `
`;
+exports[`components/View prop "lang" ar 1`] = `
+
+`;
+
+exports[`components/View prop "lang" fr 1`] = `
+
+`;
+
+exports[`components/View prop "lang" undefined 1`] = `
+
+`;
+
+exports[`components/View prop "lang" with dir 1`] = `
+
+`;
+
exports[`components/View prop "nativeID" value is set 1`] = `
{
});
});
- test('allows "dir" to be overridden', () => {
- const { container } = render();
- expect(container.firstChild).toMatchSnapshot();
+ describe('prop "dir"', () => {
+ test('value is "ltr"', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ test('value is "rtl"', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+ });
});
describe('prop "href"', () => {
@@ -141,6 +148,28 @@ describe('components/View', () => {
});
});
+ describe('prop "lang"', () => {
+ test('undefined', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ test('fr', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ test('ar', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ test('with dir', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+ });
+
describe('prop "nativeID"', () => {
test('value is set', () => {
const { container } = render();
diff --git a/packages/react-native-web/src/exports/View/index.js b/packages/react-native-web/src/exports/View/index.js
index a1052285..a24c010a 100644
--- a/packages/react-native-web/src/exports/View/index.js
+++ b/packages/react-native-web/src/exports/View/index.js
@@ -21,7 +21,7 @@ import usePlatformMethods from '../../modules/usePlatformMethods';
import useResponderEvents from '../../modules/useResponderEvents';
import StyleSheet from '../StyleSheet';
import TextAncestorContext from '../Text/TextAncestorContext';
-import RTLContext from '../../modules/RTLContext';
+import { useLocaleContext, getLocaleDirection } from '../../modules/useLocale';
const forwardPropsList = {
...forwardedProps.defaultProps,
@@ -77,7 +77,7 @@ const View: React.AbstractComponent =
const hasTextAncestor = React.useContext(TextAncestorContext);
const hostRef = React.useRef(null);
- const isRTL = React.useContext(RTLContext);
+ const { direction: contextDirection } = useLocaleContext();
useElementLayout(hostRef, onLayout);
useResponderEvents(hostRef, {
@@ -101,8 +101,12 @@ const View: React.AbstractComponent =
let component = 'div';
+ const langDirection = props.lang != null ? getLocaleDirection(props.lang) : null;
+ const componentDirection = props.dir || langDirection;
+ const writingDirection = componentDirection || contextDirection;
+
const supportedProps = pickProps(rest);
- supportedProps.isRTL = props.dir ? props.dir === 'rtl' : isRTL;
+ supportedProps.dir = componentDirection;
supportedProps.style = [styles.view$raw, hasTextAncestor && styles.inline, props.style];
if (props.href != null) {
component = 'a';
@@ -125,9 +129,7 @@ const View: React.AbstractComponent =
supportedProps.ref = setRef;
- const element = createElement(component, supportedProps);
-
- return {element};
+ return createElement(component, supportedProps, { writingDirection });
}
);
diff --git a/packages/react-native-web/src/exports/View/types.js b/packages/react-native-web/src/exports/View/types.js
index 8e0ef8a1..3787dde3 100644
--- a/packages/react-native-web/src/exports/View/types.js
+++ b/packages/react-native-web/src/exports/View/types.js
@@ -116,6 +116,7 @@ export type ViewProps = {
children?: ?any,
dir?: 'ltr' | 'rtl',
focusable?: ?boolean,
+ lang?: string,
nativeID?: ?string,
onBlur?: (e: any) => void,
onClick?: (e: any) => void,
diff --git a/packages/react-native-web/src/exports/createElement/index.js b/packages/react-native-web/src/exports/createElement/index.js
index 95e4b282..557c277d 100644
--- a/packages/react-native-web/src/exports/createElement/index.js
+++ b/packages/react-native-web/src/exports/createElement/index.js
@@ -10,17 +10,27 @@
import AccessibilityUtil from '../../modules/AccessibilityUtil';
import createDOMProps from '../../modules/createDOMProps';
import React from 'react';
+import { LocaleProvider } from '../../modules/useLocale';
-const createElement = (component, props, ...children) => {
+const createElement = (component, props, options) => {
// Use equivalent platform elements where possible.
let accessibilityComponent;
if (component && component.constructor === String) {
accessibilityComponent = AccessibilityUtil.propsToAccessibilityComponent(props);
}
const Component = accessibilityComponent || component;
- const domProps = createDOMProps(Component, props);
+ const domProps = createDOMProps(Component, props, options);
- return React.createElement(Component, domProps, ...children);
+ const element = React.createElement(Component, domProps);
+
+ // Update locale context if element's writing direction prop changes
+ const elementWithLocaleProvider = domProps.dir ? (
+
+ ) : (
+ element
+ );
+
+ return elementWithLocaleProvider;
};
export default createElement;
diff --git a/packages/react-native-web/src/exports/useLocaleContext/index.js b/packages/react-native-web/src/exports/useLocaleContext/index.js
new file mode 100644
index 00000000..acc5ce24
--- /dev/null
+++ b/packages/react-native-web/src/exports/useLocaleContext/index.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Nicolas Gallagher.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+export { useLocaleContext } from '../../modules/useLocale';
diff --git a/packages/react-native-web/src/index.js b/packages/react-native-web/src/index.js
index c7d8aacb..21fbe356 100644
--- a/packages/react-native-web/src/index.js
+++ b/packages/react-native-web/src/index.js
@@ -77,4 +77,5 @@ export { default as DeviceEventEmitter } from './exports/DeviceEventEmitter';
// hooks
export { default as useColorScheme } from './exports/useColorScheme';
+export { useLocaleContext } from './exports/useLocaleContext';
export { default as useWindowDimensions } from './exports/useWindowDimensions';
diff --git a/packages/react-native-web/src/modules/RTLContext/index.js b/packages/react-native-web/src/modules/RTLContext/index.js
deleted file mode 100644
index bae1b5ea..00000000
--- a/packages/react-native-web/src/modules/RTLContext/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @flow strict
- */
-
-import type { Context } from 'react';
-
-import { createContext } from 'react';
-
-const RTLContext = createContext(false);
-export default (RTLContext: Context);
diff --git a/packages/react-native-web/src/modules/createDOMProps/index.js b/packages/react-native-web/src/modules/createDOMProps/index.js
index cc8af116..73fa05e6 100644
--- a/packages/react-native-web/src/modules/createDOMProps/index.js
+++ b/packages/react-native-web/src/modules/createDOMProps/index.js
@@ -40,7 +40,7 @@ const pointerEventsStyles = StyleSheet.create({
}
});
-const createDOMProps = (elementType, props) => {
+const createDOMProps = (elementType, props, options) => {
if (!props) {
props = emptyObject;
}
@@ -100,7 +100,6 @@ const createDOMProps = (elementType, props) => {
pointerEvents,
style,
testID,
- isRTL,
// Rest
...domProps
} = props;
@@ -319,7 +318,7 @@ const createDOMProps = (elementType, props) => {
// Resolve styles
const [className, inlineStyle] = StyleSheet(
[style, pointerEvents && pointerEventsStyles[pointerEvents]],
- isRTL
+ { writingDirection: options ? options.writingDirection : 'ltr' }
);
if (className) {
domProps.className = className;
diff --git a/packages/react-native-web/src/modules/useLocale/index.js b/packages/react-native-web/src/modules/useLocale/index.js
new file mode 100644
index 00000000..11c51517
--- /dev/null
+++ b/packages/react-native-web/src/modules/useLocale/index.js
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) Nicolas Gallagher.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict
+ */
+
+import type { Node } from 'react';
+
+import React, { createContext, useContext } from 'react';
+import { isLocaleRTL } from './isLocaleRTL';
+
+type Locale = string;
+type WritingDirection = 'ltr' | 'rtl';
+
+type LocaleValue = {
+ // Locale writing direction.
+ direction: WritingDirection,
+ // Locale BCP47 language code: https://www.ietf.org/rfc/bcp/bcp47.txt
+ locale: ?Locale
+};
+
+type ProviderProps = {
+ ...LocaleValue,
+ children: any
+};
+
+const defaultLocale = {
+ direction: 'ltr',
+ locale: 'en-US'
+};
+
+const LocaleContext = createContext(defaultLocale);
+
+export function getLocaleDirection(locale: Locale): WritingDirection {
+ return isLocaleRTL(locale) ? 'rtl' : 'ltr';
+}
+
+export function LocaleProvider(props: ProviderProps): Node {
+ const { direction, locale, children } = props;
+ const needsContext = direction || locale;
+
+ return needsContext ? (
+
+ ) : (
+ children
+ );
+}
+
+export function useLocaleContext(): LocaleValue {
+ return useContext(LocaleContext);
+}
diff --git a/packages/react-native-web/src/modules/useLocale/isLocaleRTL.js b/packages/react-native-web/src/modules/useLocale/isLocaleRTL.js
new file mode 100644
index 00000000..202c3e19
--- /dev/null
+++ b/packages/react-native-web/src/modules/useLocale/isLocaleRTL.js
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) Nicolas Gallagher.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+const rtlScripts = new Set([
+ 'Arab',
+ 'Syrc',
+ 'Samr',
+ 'Mand',
+ 'Thaa',
+ 'Mend',
+ 'Nkoo',
+ 'Adlm',
+ 'Rohg',
+ 'Hebr'
+]);
+
+const rtlLangs = new Set([
+ 'ae', // Avestan
+ 'ar', // Arabic
+ 'arc', // Aramaic
+ 'bcc', // Southern Balochi
+ 'bqi', // Bakthiari
+ 'ckb', // Sorani
+ 'dv', // Dhivehi
+ 'fa',
+ 'far', // Persian
+ 'glk', // Gilaki
+ 'he',
+ 'iw', // Hebrew
+ 'khw', // Khowar
+ 'ks', // Kashmiri
+ 'ku', // Kurdish
+ 'mzn', // Mazanderani
+ 'nqo', // N'Ko
+ 'pnb', // Western Punjabi
+ 'ps', // Pashto
+ 'sd', // Sindhi
+ 'ug', // Uyghur
+ 'ur', // Urdu
+ 'yi' // Yiddish
+]);
+
+const cache = new Map();
+
+/**
+ * Determine the writing direction of a locale
+ */
+export function isLocaleRTL(locale: string): boolean {
+ const cachedRTL = cache.get(locale);
+ if (cachedRTL) {
+ return cachedRTL;
+ }
+
+ let isRTL = false;
+ // $FlowFixMe
+ if (Intl.Locale) {
+ // $FlowFixMe
+ const script = new Intl.Locale(locale).maximize().script;
+ isRTL = rtlScripts.has(script);
+ } else {
+ // Fallback to inferring from language
+ const lang = locale.split('-')[0];
+ isRTL = rtlLangs.has(lang);
+ }
+
+ cache.set(locale, isRTL);
+ return isRTL;
+}
diff --git a/yarn.lock b/yarn.lock
index ad961ff3..7903e705 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7929,10 +7929,10 @@ styled-jsx@5.0.0:
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.0.tgz#816b4b92e07b1786c6b7111821750e0ba4d26e77"
integrity sha512-qUqsWoBquEdERe10EW8vLp3jT25s/ssG1/qX5gZ4wu15OZpmSMFI2v+fWlRhLfykA5rFtlJ1ME8A8pm/peV4WA==
-styleq@0.0.0-b4fc5da:
- version "0.0.0-b4fc5da"
- resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.0.0-b4fc5da.tgz#d8b3ccadca6c08eff84c43490db5053d63a749b0"
- integrity sha512-w2DjdNufdrzl2pos9L+t76OJyxgyHMLXAYDrCchlBfjnB4ZHWVdix5wUD2HTVT/qSekV2084BETeGLHhZD2dSw==
+styleq@0.0.0-afc6b2c59:
+ version "0.0.0-afc6b2c59"
+ resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.0.0-afc6b2c59.tgz#d83f1d462ee9be71405bae79a44416350ca216d1"
+ integrity sha512-/DoJ+Moi4+wn14JyLfWaghuub5urI//6bZHa1rCC6QtNMVI3LlpxsHLCaoPuD1POniViJaOPNRrLAesGT+nnow==
supports-color@^2.0.0:
version "2.0.0"