├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .npmrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── src
├── AutoDirectionProvider.jsx
├── DirectionProvider.jsx
├── constants.js
├── proptypes
│ ├── brcast.js
│ └── direction.js
└── withDirection.jsx
└── tests
├── .eslintrc
├── AutoDirectionProvider_test.jsx
├── DirectionProvider_test.jsx
├── _helpers.jsx
├── mocks
└── brcast_mock.js
└── withDirection_test.jsx
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["airbnb"],
3 | "plugins": [
4 | ["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }],
5 | ],
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "airbnb",
4 | "env": {
5 | "browser": true,
6 | "node": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # only apps should have lock files
61 | npm-shrinkwrap.json
62 | package-lock.json
63 | yarn.lock
64 |
65 | # babel output
66 | dist
67 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # only apps should have lock files
61 | npm-shrinkwrap.json
62 | package-lock.json
63 | yarn.lock
64 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "16"
4 | - "14"
5 | - "12"
6 | - "10"
7 | - "8"
8 | - "6"
9 | - "4"
10 | before_install:
11 | - 'nvm install-latest-npm'
12 | install:
13 | - 'if [ "${TRAVIS_NODE_VERSION}" = "0.6" ] || [ "${TRAVIS_NODE_VERSION}" = "0.9" ]; then nvm install --latest-npm 0.8 && npm install && nvm use "${TRAVIS_NODE_VERSION}"; else npm install; fi;'
14 | script:
15 | - 'if [ -n "${LINT-}" ]; then npm run lint; fi'
16 | - 'if [ "${TEST-}" = true ]; then npm run test:only; fi'
17 | env:
18 | global:
19 | - TEST=true
20 | matrix:
21 | - REACT=0.14
22 | - REACT=15.4
23 | - REACT=15
24 | - REACT=16
25 | sudo: false
26 | matrix:
27 | fast_finish: true
28 | include:
29 | env: LINT=true TEST=false
30 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## v1.4.0
4 | - [New] `DirectionProvider`: add `lang` prop
5 | - [Deps] update `airbnb-prop-types`, `direction`, `hoist-non-react-statics`, `object.assign`, `object.values`, `prop-types`
6 | - [Dev Deps] update `babel-preset-airbnb`, `casual`, `chai, `enzyme`, `enzyme-adapter-react-helper`, `eslint`, `eslint-config-airbnb`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `rimraf`, `safe-publish-latest`, `sinon-chai`, `sinon-sandbox`, `webpack`
7 |
8 | ## v1.3.1
9 | - [Deps] Update hoist-non-react-statics to 3.3.0
10 | - [Deps] update `airbnb-prop-types`, `deepmerge`, `direction`, `hoist-non-react-statics`, `prop-types`
11 | - [Dev Deps] update `babel-plugin-transform-replace-object-assign`, `babel-preset-airbnb`, `chai-enzyme`, `enzyme-adapter-react-helper`, `eslint`, `eslint-config-airbnb`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `sinon`, `sinon-chai`, `sinon-sandbox`
12 |
13 | ## v1.3.0
14 | - [New] Add `AutoDirectionProvider` component for automatically detected direction.
15 |
16 | ## v1.2.0
17 | - [New] Add `inline` option; if specified, will render a `span` instead of a `div`.
18 |
19 | ## v1.1.0
20 | - [New] increase peerDep/devDep range for react and bump enzyme (#2)
21 | - [Deps] update `hoist-non-react-statics`, `prop-types`
22 | - [Dev Deps] update `babel-jest`, `chai`, `eslint`, `eslint-config-airbnb`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `jest`, `rimraf`, `sinon`, `sinon-chai`, `webpack`
23 |
24 | ## v1.0.1
25 | - [Fix] allow down to React v15.0
26 | - [Deps] update `airbnb-prop-types`, `deepmerge`, `hoist-non-react-statics`
27 | - [Dev Deps] update `babel-cli`, `casual`, `eslint`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `sinon-chai`, `sinon`, `webpack`; remove `eslint-config-airbnb-base`
28 | - [Dev Deps] add `babel-jest`, which needs to be explicitly a top-level dev dep for npm 2
29 | - [Tests] test on node 4, but not node 7
30 |
31 | ## v1.0.0
32 | - Initial release
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Airbnb
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-with-direction [![Version Badge][npm-version-svg]][package-url]
2 |
3 | [![Build Status][travis-svg]][travis-url]
4 | [![dependency status][deps-svg]][deps-url]
5 | [![dev dependency status][dev-deps-svg]][dev-deps-url]
6 | [![License][license-image]][license-url]
7 | [![Downloads][downloads-image]][downloads-url]
8 |
9 | [![npm badge][npm-badge-png]][package-url]
10 |
11 | Components to support both right-to-left (RTL) and left-to-right (LTR) layouts in React.
12 |
13 | Supporting RTL or switching between different directions can be tricky. Most browsers have [built-in support](https://www.w3.org/International/questions/qa-html-dir) for displaying markup like paragraphs, lists, and tables. But what about interactive or complex custom UI components? In a right-to-left layout, a photo carousel should advance in the opposite direction, and the primary tab in a navigation control should the rightmost, for example.
14 |
15 | This package provides components to simplify that effort.
16 |
17 | ## withDirection
18 |
19 | Use `withDirection` when your component needs to change based on the layout direction. `withDirection` is an HOC that consumes the direction from React context and passes it as a `direction` prop to the wrapped component. The wrapped component can then pivot its logic to accommodate each direction.
20 |
21 | Usage example:
22 |
23 | ```js
24 | import withDirection, { withDirectionPropTypes, DIRECTIONS } from 'react-with-direction';
25 |
26 | function ForwardsLabel({ direction }) {
27 | return (
28 |
29 | Forwards
30 | {direction === DIRECTIONS.LTR &&

}
31 | {direction === DIRECTIONS.RTL &&

}
32 |
33 | );
34 | }
35 | ForwardsLabel.propTypes = {
36 | ...withDirectionPropTypes,
37 | };
38 |
39 | export default withDirection(ForwardsLabel);
40 | ```
41 |
42 | ## DirectionProvider
43 |
44 | Use `DirectionProvider` at the top of your app to set the direction context, which can then be consumed by components using `withDirection`.
45 |
46 | You should set the `direction` prop based on the language of the content being rendered; for example, `DIRECTIONS.RTL` (right-to-left) for Arabic or Hebrew, or `DIRECTIONS.LTR` (left-to-right) for English or most other languages.
47 |
48 | `DirectionProvider` components can also be nested, so that the direction can be overridden for certain branches of the React tree.
49 |
50 | `DirectionProvider` will render its children inside of a `` element with a `dir` attribute set to match the `direction` prop, for example: `
`. This maintains consistency when being rendered in a browser. To render inside of a `
` instead of a div, set the `inline` prop to `true`.
51 |
52 | Usage example:
53 |
54 | ```js
55 | import DirectionProvider, { DIRECTIONS } from 'react-with-direction/dist/DirectionProvider';
56 | ```
57 |
58 | ```jsx
59 |
60 |
61 |
62 |
63 |
64 | ```
65 |
66 | To set the `lang` attribute on the wrapping element, provide the `lang` prop to `DirectionProvider`.
67 |
68 | Usage example:
69 |
70 | ```jsx
71 | import DirectionProvider, { DIRECTIONS } from 'react-with-direction/dist/DirectionProvider';
72 |
73 |
74 |
75 |
76 |
77 |
78 | ```
79 |
80 | Note that `lang` and `direction` are independent – `lang` only sets the attribute on the wrapping element.
81 |
82 | ## AutoDirectionProvider
83 |
84 | Use `AutoDirectionProvider` around, for example, user-generated content where the text direction is unknown or may change. This renders a `DirectionProvider` with the `direction` prop automatically set based on the `text` prop provided.
85 |
86 | Direction will be determined based on the first strong LTR/RTL character in the `text` string. Strings with no strong direction (e.g., numbers) will inherit the direction from its nearest `DirectionProvider` ancestor or default to LTR.
87 |
88 | Usage example:
89 |
90 | ```js
91 | import AutoDirectionProvider from 'react-with-direction/dist/AutoDirectionProvider';
92 | ```
93 |
94 | ```js
95 |
96 |
97 | {userGeneratedContent}
98 |
99 |
100 | ```
101 |
102 | `AutoDirectionProvider` also supports the `lang` prop in the same way as `DirectionProvider` does.
103 |
104 | [package-url]: https://npmjs.org/package/react-with-direction
105 | [npm-version-svg]: http://versionbadg.es/airbnb/react-with-direction.svg
106 | [travis-svg]: https://travis-ci.org/airbnb/react-with-direction.svg
107 | [travis-url]: https://travis-ci.org/airbnb/react-with-direction
108 | [deps-svg]: https://david-dm.org/airbnb/react-with-direction.svg
109 | [deps-url]: https://david-dm.org/airbnb/react-with-direction
110 | [dev-deps-svg]: https://david-dm.org/airbnb/react-with-direction/dev-status.svg
111 | [dev-deps-url]: https://david-dm.org/airbnb/react-with-direction#info=devDependencies
112 | [npm-badge-png]: https://nodei.co/npm/react-with-direction.png?downloads=true&stars=true
113 | [license-image]: http://img.shields.io/npm/l/react-with-direction.svg
114 | [license-url]: LICENSE
115 | [downloads-image]: http://img.shields.io/npm/dm/react-with-direction.svg
116 | [downloads-url]: http://npm-stat.com/charts.html?package=react-with-direction
117 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: [
3 | './tests',
4 | ],
5 | setupTestFrameworkScriptFile: './tests/_helpers.jsx',
6 | testEnvironment: 'node',
7 | testRegex: '.*(\\.|/|_)(test)\\.jsx?$',
8 | };
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-with-direction",
3 | "version": "1.4.0",
4 | "description": "Components to provide and consume RTL or LTR direction in React",
5 | "author": "Yaniv Zimet ",
6 | "repository": "airbnb/react-with-direction",
7 | "homepage": "https://github.com/airbnb/react-with-direction#readme",
8 | "bugs": "https://github.com/airbnb/react-with-direction/issues",
9 | "main": "dist/withDirection",
10 | "license": "MIT",
11 | "scripts": {
12 | "prebuild": "npm run clean",
13 | "build": "babel src --out-dir dist",
14 | "lint": "eslint .",
15 | "pretest": "npm run lint",
16 | "pretest:only": "npm run react",
17 | "test:only": "jest",
18 | "test": "npm run test:only",
19 | "test:watch": "npm run test:only -- --watchAll",
20 | "clean": "rimraf dist",
21 | "react": "enzyme-adapter-react-install 16",
22 | "prepublish": "safe-publish-latest && npm run build"
23 | },
24 | "dependencies": {
25 | "airbnb-prop-types": "^2.16.0",
26 | "brcast": "^2.0.2",
27 | "deepmerge": "^1.5.2",
28 | "direction": "^1.0.4",
29 | "hoist-non-react-statics": "^3.3.2",
30 | "object.assign": "^4.1.2",
31 | "object.values": "^1.1.5",
32 | "prop-types": "^15.7.2"
33 | },
34 | "devDependencies": {
35 | "babel-cli": "^6.26.0",
36 | "babel-jest": "^21.2.0",
37 | "babel-plugin-transform-replace-object-assign": "^1.0.0",
38 | "babel-preset-airbnb": "^2.6.0",
39 | "casual": "^1.6.2",
40 | "chai": "^4.3.4",
41 | "chai-enzyme": "^1.0.0-beta.1",
42 | "cheerio": "=1.0.0-rc.3",
43 | "enzyme": "^3.11.0",
44 | "enzyme-adapter-react-helper": "^1.3.9",
45 | "eslint": "^7.32.0",
46 | "eslint-config-airbnb": "^18.2.1",
47 | "eslint-plugin-import": "^2.24.2",
48 | "eslint-plugin-jsx-a11y": "^6.4.1",
49 | "eslint-plugin-react": "^7.26.1",
50 | "jest": "^21.2.1",
51 | "react": "^0.14 || ^15 || ^16",
52 | "react-dom": "^0.14 || ^15 || ^16",
53 | "rimraf": "^2.7.1",
54 | "safe-publish-latest": "^1.1.4",
55 | "sinon": "^4.5.0",
56 | "sinon-chai": "^3.7.0",
57 | "sinon-sandbox": "^2.0.6",
58 | "webpack": "^3.12.0"
59 | },
60 | "peerDependencies": {
61 | "react": "^0.14 || ^15 || ^16",
62 | "react-dom": "^0.14 || ^15 || ^16"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/AutoDirectionProvider.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import { forbidExtraProps } from 'airbnb-prop-types';
4 | import getDirection from 'direction';
5 | import directionPropType from './proptypes/direction';
6 | import DirectionProvider from './DirectionProvider';
7 | import withDirection from './withDirection';
8 |
9 | const propTypes = forbidExtraProps({
10 | children: PropTypes.node.isRequired,
11 | direction: directionPropType.isRequired,
12 | inline: PropTypes.bool,
13 | text: PropTypes.string.isRequired,
14 | lang: PropTypes.string,
15 | });
16 |
17 | const defaultProps = {
18 | inline: false,
19 | };
20 |
21 | function AutoDirectionProvider({
22 | children,
23 | direction,
24 | inline,
25 | text,
26 | lang,
27 | }) {
28 | const textDirection = getDirection(text);
29 | const dir = textDirection === 'neutral' ? direction : textDirection;
30 |
31 | return (
32 |
37 | {React.Children.only(children)}
38 |
39 | );
40 | }
41 |
42 | AutoDirectionProvider.propTypes = propTypes;
43 | AutoDirectionProvider.defaultProps = defaultProps;
44 |
45 | export default withDirection(AutoDirectionProvider);
46 |
--------------------------------------------------------------------------------
/src/DirectionProvider.jsx:
--------------------------------------------------------------------------------
1 | // This component provides a string to React context that is consumed by the
2 | // withDirection higher-order component. We can use this to give access to
3 | // the current layout direction for components to use.
4 |
5 | import PropTypes from 'prop-types';
6 | import React from 'react';
7 | import { forbidExtraProps } from 'airbnb-prop-types';
8 | import brcast from 'brcast';
9 | import brcastShape from './proptypes/brcast';
10 | import directionPropType from './proptypes/direction';
11 | import { DIRECTIONS, CHANNEL } from './constants';
12 |
13 | const propTypes = forbidExtraProps({
14 | children: PropTypes.node.isRequired,
15 | direction: directionPropType.isRequired,
16 | inline: PropTypes.bool,
17 | lang: PropTypes.string,
18 | });
19 |
20 | const defaultProps = {
21 | inline: false,
22 | };
23 |
24 | const childContextTypes = {
25 | [CHANNEL]: brcastShape,
26 | };
27 |
28 | export { DIRECTIONS };
29 |
30 | export default class DirectionProvider extends React.Component {
31 | constructor(props) {
32 | super(props);
33 | this.broadcast = brcast(props.direction);
34 | }
35 |
36 | getChildContext() {
37 | return {
38 | [CHANNEL]: this.broadcast,
39 | };
40 | }
41 |
42 | componentWillReceiveProps(nextProps) {
43 | if (this.props.direction !== nextProps.direction) {
44 | this.broadcast.setState(nextProps.direction);
45 | }
46 | }
47 |
48 | render() {
49 | const { children, direction, inline, lang } = this.props;
50 | const Tag = inline ? 'span' : 'div';
51 | return (
52 |
53 | {React.Children.only(children)}
54 |
55 | );
56 | }
57 | }
58 |
59 | DirectionProvider.propTypes = propTypes;
60 | DirectionProvider.defaultProps = defaultProps;
61 | DirectionProvider.childContextTypes = childContextTypes;
62 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const CHANNEL = '__direction__';
2 |
3 | export const DIRECTIONS = {
4 | LTR: 'ltr',
5 | RTL: 'rtl',
6 | };
7 |
--------------------------------------------------------------------------------
/src/proptypes/brcast.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default PropTypes.shape({
4 | getState: PropTypes.func,
5 | setState: PropTypes.func,
6 | subscribe: PropTypes.func,
7 | });
8 |
--------------------------------------------------------------------------------
/src/proptypes/direction.js:
--------------------------------------------------------------------------------
1 | import values from 'object.values';
2 | import PropTypes from 'prop-types';
3 |
4 | import { DIRECTIONS } from '../constants';
5 |
6 | export default PropTypes.oneOf(values(DIRECTIONS));
7 |
--------------------------------------------------------------------------------
/src/withDirection.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-foreign-prop-types */
2 | // This higher-order component consumes a string from React context that is
3 | // provided by the DirectionProvider component.
4 | // We can use this to conditionally switch layout/direction for right-to-left layouts.
5 |
6 | import React from 'react';
7 | import hoistNonReactStatics from 'hoist-non-react-statics';
8 | import deepmerge from 'deepmerge';
9 | import getComponentName from 'airbnb-prop-types/build/helpers/getComponentName';
10 | import { CHANNEL, DIRECTIONS } from './constants';
11 | import brcastShape from './proptypes/brcast';
12 | import directionPropType from './proptypes/direction';
13 |
14 | const contextTypes = {
15 | [CHANNEL]: brcastShape,
16 | };
17 |
18 | export { DIRECTIONS };
19 |
20 | // set a default direction so that a component wrapped with this HOC can be
21 | // used even without a DirectionProvider ancestor in its react tree.
22 | const defaultDirection = DIRECTIONS.LTR;
23 |
24 | // export for convenience, in order for components to spread these onto their propTypes
25 | export const withDirectionPropTypes = {
26 | direction: directionPropType.isRequired,
27 | };
28 |
29 | export default function withDirection(WrappedComponent) {
30 | class WithDirection extends React.Component {
31 | constructor(props, context) {
32 | super(props, context);
33 | this.state = {
34 | direction: context[CHANNEL] ? context[CHANNEL].getState() : defaultDirection,
35 | };
36 | }
37 |
38 | componentDidMount() {
39 | if (this.context[CHANNEL]) {
40 | // subscribe to future direction changes
41 | this.channelUnsubscribe = this.context[CHANNEL].subscribe((direction) => {
42 | this.setState({ direction });
43 | });
44 | }
45 | }
46 |
47 | componentWillUnmount() {
48 | if (this.channelUnsubscribe) {
49 | this.channelUnsubscribe();
50 | }
51 | }
52 |
53 | render() {
54 | const { direction } = this.state;
55 |
56 | return (
57 |
61 | );
62 | }
63 | }
64 |
65 | const wrappedComponentName = getComponentName(WrappedComponent) || 'Component';
66 |
67 | WithDirection.WrappedComponent = WrappedComponent;
68 | WithDirection.contextTypes = contextTypes;
69 | WithDirection.displayName = `withDirection(${wrappedComponentName})`;
70 | if (WrappedComponent.propTypes) {
71 | WithDirection.propTypes = deepmerge({}, WrappedComponent.propTypes);
72 | delete WithDirection.propTypes.direction;
73 | }
74 | if (WrappedComponent.defaultProps) {
75 | WithDirection.defaultProps = deepmerge({}, WrappedComponent.defaultProps);
76 | }
77 |
78 | return hoistNonReactStatics(WithDirection, WrappedComponent);
79 | }
80 |
--------------------------------------------------------------------------------
/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tests/AutoDirectionProvider_test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { expect } from 'chai';
3 | import { shallow } from 'enzyme';
4 |
5 | import AutoDirectionProvider from '../src/AutoDirectionProvider';
6 | import DirectionProvider from '../src/DirectionProvider';
7 | import { DIRECTIONS, CHANNEL } from '../src/constants';
8 | import mockBrcast from './mocks/brcast_mock';
9 |
10 | describe('', () => {
11 | it('renders a DirectionProvider', () => {
12 | const wrapper = shallow((
13 |
14 |
15 |
16 | )).dive();
17 |
18 | expect(wrapper).to.have.exactly(1).descendants(DirectionProvider);
19 | });
20 |
21 | describe('direction prop', () => {
22 | it('is LTR correct for LTR strings', () => {
23 | const wrapper = shallow((
24 |
25 |
26 |
27 | )).dive();
28 |
29 | expect(wrapper.find(DirectionProvider)).to.have.prop('direction', DIRECTIONS.LTR);
30 | });
31 |
32 | it('is RTL correct for RTL strings', () => {
33 | const wrapper = shallow((
34 |
35 |
36 |
37 | )).dive();
38 |
39 | expect(wrapper.find(DirectionProvider)).to.have.prop('direction', DIRECTIONS.RTL);
40 | });
41 |
42 | it('is inherited from context for neutral strings', () => {
43 | const wrapper = shallow(
44 | (
45 |
46 |
47 |
48 | ), {
49 | context: {
50 | [CHANNEL]: mockBrcast({
51 | data: DIRECTIONS.RTL,
52 | }),
53 | },
54 | },
55 | ).dive();
56 |
57 | expect(wrapper.find(DirectionProvider)).to.have.prop('direction', DIRECTIONS.RTL);
58 | });
59 | });
60 |
61 | it('renders its children', () => {
62 | const children = Foo
;
63 |
64 | const wrapper = shallow((
65 |
66 | {children}
67 |
68 | )).dive();
69 |
70 | expect(wrapper).to.contain(children);
71 | });
72 |
73 | it('passes the inline prop to DirectionProvider', () => {
74 | let wrapper = shallow((
75 |
76 |
77 |
78 | )).dive();
79 |
80 | expect(wrapper.find(DirectionProvider)).to.have.prop('inline', false);
81 |
82 | wrapper = shallow((
83 |
84 |
85 |
86 | )).dive();
87 |
88 | expect(wrapper.find(DirectionProvider)).to.have.prop('inline', true);
89 | });
90 |
91 | it('passes the lang prop to DirectionProvider', () => {
92 | const wrapper = shallow((
93 |
94 |
95 |
96 | )).dive();
97 |
98 | expect(wrapper.find(DirectionProvider)).to.have.prop('lang', 'en');
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/tests/DirectionProvider_test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { expect } from 'chai';
3 | import { shallow } from 'enzyme';
4 | import sinon from 'sinon-sandbox';
5 |
6 | import DirectionProvider from '../src/DirectionProvider';
7 | import { DIRECTIONS } from '../src/constants';
8 |
9 | describe('', () => {
10 | let children;
11 | beforeEach(() => {
12 | children = Foo
;
13 | });
14 |
15 | it('renders its children', () => {
16 | const wrapper = shallow(
17 | {children},
18 | );
19 | expect(wrapper.contains(children)).to.eq(true);
20 | });
21 |
22 | it('renders a wrapping div with a dir attribute', () => {
23 | const direction = DIRECTIONS.RTL;
24 | const wrapper = shallow(
25 | {children},
26 | );
27 | expect(wrapper).to.have.type('div');
28 | expect(wrapper).to.have.prop('dir', direction);
29 | });
30 |
31 | it('renders a wrapping span when the inline prop is true', () => {
32 | const direction = DIRECTIONS.RTL;
33 | const wrapper = shallow(
34 | {children},
35 | );
36 | expect(wrapper).to.have.type('span');
37 | expect(wrapper).to.have.prop('dir', direction);
38 | });
39 |
40 | it('renders a lang attribute when the lang prop is set', () => {
41 | const direction = DIRECTIONS.RTL;
42 | const wrapper = shallow(
43 | {children},
44 | );
45 | expect(wrapper).to.have.prop('lang', 'ar');
46 | });
47 |
48 | it('broadcasts the direction when the direction prop changes', () => {
49 | const direction = DIRECTIONS.LTR;
50 | const nextDirection = DIRECTIONS.RTL;
51 | const wrapper = shallow(
52 | {children},
53 | );
54 | const broadcast = wrapper.instance().broadcast;
55 | const broadcastSpy = sinon.spy(broadcast, 'setState');
56 | wrapper.setProps({ direction: nextDirection });
57 | expect(broadcastSpy).to.have.callCount(1);
58 | });
59 |
60 | it('does not broadcast the direction when the direction prop stays the same', () => {
61 | const direction = DIRECTIONS.LTR;
62 | const nextDirection = DIRECTIONS.LTR;
63 | const wrapper = shallow(
64 | {children},
65 | );
66 | const broadcast = wrapper.instance().broadcast;
67 | const broadcastSpy = sinon.spy(broadcast, 'setState');
68 | wrapper.setProps({ direction: nextDirection });
69 | expect(broadcastSpy).to.have.callCount(0);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/tests/_helpers.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import chai from 'chai';
3 | import sinonChai from 'sinon-chai';
4 | import sinon from 'sinon-sandbox';
5 | import chaiEnzyme from 'chai-enzyme';
6 |
7 | import configure from 'enzyme-adapter-react-helper';
8 |
9 | configure({ disableLifecycleMethods: true });
10 |
11 | chai.use(sinonChai);
12 | chai.use(chaiEnzyme());
13 |
14 | afterEach(() => {
15 | sinon.restore();
16 | });
17 |
--------------------------------------------------------------------------------
/tests/mocks/brcast_mock.js:
--------------------------------------------------------------------------------
1 | import casual from 'casual';
2 | import sinon from 'sinon-sandbox';
3 |
4 | export default (overrides = {}) => {
5 | const brcast = {
6 | data: casual.string,
7 | setState: sinon.stub(),
8 | unsubscribe: sinon.stub(),
9 | ...overrides,
10 | };
11 |
12 | brcast.getState = sinon.stub().returns(brcast.data);
13 | brcast.subscribe = sinon.stub().returns(brcast.unsubscribe);
14 |
15 | return {
16 | ...brcast,
17 | ...overrides,
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/tests/withDirection_test.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import { expect } from 'chai';
4 | import { shallow, render } from 'enzyme';
5 | import sinon from 'sinon-sandbox';
6 |
7 | import DirectionProvider from '../src/DirectionProvider';
8 | import withDirection, { withDirectionPropTypes } from '../src/withDirection';
9 | import { DIRECTIONS, CHANNEL } from '../src/constants';
10 | import mockBrcast from './mocks/brcast_mock';
11 |
12 | function getWrappedComponent(expectedDirection) {
13 | function MyComponent({ animal, direction }) {
14 | expect(direction).to.equal(expectedDirection);
15 | return (
16 | {`My direction is ${direction} and my animal is ${animal}.`}
17 | );
18 | }
19 | MyComponent.propTypes = {
20 | ...withDirectionPropTypes,
21 | animal: PropTypes.string,
22 | };
23 | MyComponent.defaultProps = {
24 | animal: 'dog',
25 | };
26 |
27 | return withDirection(MyComponent);
28 | }
29 |
30 | describe('withDirection()', () => {
31 | it('has a wrapped displayName', () => {
32 | const Wrapped = getWrappedComponent(DIRECTIONS.LTR);
33 | expect(Wrapped.displayName).to.equal('withDirection(MyComponent)');
34 | });
35 |
36 | it('defaults direction to LTR', () => {
37 | const Wrapped = getWrappedComponent(DIRECTIONS.LTR);
38 | render();
39 | });
40 |
41 | it('passes direction from context to the wrapped component', () => {
42 | const Wrapped = getWrappedComponent(DIRECTIONS.RTL);
43 | render(
44 |
45 |
46 |
47 |
48 | ,
49 | );
50 | });
51 |
52 | describe('lifecycle methods', () => {
53 | let brcast;
54 | beforeEach(() => {
55 | const unsubscribe = sinon.stub();
56 | brcast = mockBrcast({
57 | data: DIRECTIONS.LTR,
58 | subscribe: sinon.stub().yields(DIRECTIONS.RTL).returns(unsubscribe),
59 | unsubscribe,
60 | });
61 | });
62 |
63 | describe('with a brcast context', () => {
64 | let context;
65 | beforeEach(() => {
66 | context = {
67 | [CHANNEL]: brcast,
68 | };
69 | });
70 |
71 | it('sets state with a new direction when the context changes', () => {
72 | const Wrapped = getWrappedComponent(DIRECTIONS.LTR);
73 | const wrapper = shallow(, { context });
74 | expect(wrapper).to.have.prop('direction', DIRECTIONS.LTR);
75 |
76 | wrapper.instance().componentDidMount();
77 | wrapper.update();
78 | expect(wrapper).to.have.prop('direction', DIRECTIONS.RTL);
79 | });
80 |
81 | it('calls brcast subscribe when the component mounts', () => {
82 | const Wrapped = getWrappedComponent(DIRECTIONS.LTR);
83 | const wrapper = shallow(, { context });
84 |
85 | expect(brcast.subscribe).to.have.callCount(0);
86 | wrapper.instance().componentDidMount();
87 | expect(brcast.subscribe).to.have.callCount(1);
88 | });
89 |
90 | it('calls brcast unsubscribe when the component unmounts', () => {
91 | const Wrapped = getWrappedComponent(DIRECTIONS.LTR);
92 | const wrapper = shallow(, { context });
93 | wrapper.instance().componentDidMount();
94 |
95 | expect(brcast.unsubscribe).to.have.callCount(0);
96 | wrapper.instance().componentWillUnmount();
97 | expect(brcast.unsubscribe).to.have.callCount(1);
98 | });
99 | });
100 |
101 | describe('without a brcast context', () => {
102 | let context;
103 | beforeEach(() => {
104 | context = {
105 | [CHANNEL]: null,
106 | };
107 | });
108 |
109 | it('does not call brcast subscribe when the component mounts', () => {
110 | const Wrapped = getWrappedComponent(DIRECTIONS.LTR);
111 | const wrapper = shallow(, { context });
112 |
113 | wrapper.instance().componentDidMount();
114 | expect(brcast.subscribe).to.have.callCount(0);
115 | });
116 |
117 | it('does not call brcast unsubscribe when the component unmounts', () => {
118 | const Wrapped = getWrappedComponent(DIRECTIONS.LTR);
119 | const wrapper = shallow(, { context });
120 | wrapper.instance().componentDidMount();
121 |
122 | wrapper.instance().componentWillUnmount();
123 | expect(brcast.unsubscribe).to.have.callCount(0);
124 | });
125 | });
126 | });
127 | });
128 |
--------------------------------------------------------------------------------