diff --git a/packages/components/src/utils/rtl.js b/packages/components/src/utils/rtl.js index 3fa6c3757737a..bc312455baa3a 100644 --- a/packages/components/src/utils/rtl.js +++ b/packages/components/src/utils/rtl.js @@ -2,48 +2,84 @@ * External dependencies */ import { css } from '@emotion/core'; +import { mapKeys } from 'lodash'; +const LOWER_LEFT_REGEXP = new RegExp( /-left/g ); +const LOWER_RIGHT_REGEXP = new RegExp( /-right/g ); +const UPPER_LEFT_REGEXP = new RegExp( /Left/g ); +const UPPER_RIGHT_REGEXP = new RegExp( /Right/g ); + +/** + * Checks to see whether the document is set to rtl. + * + * @return {boolean} Whether document is RTL. + */ function getRtl() { return !! ( document && document.documentElement.dir === 'rtl' ); } /** * Simple hook to retrieve RTL direction value + * + * @return {boolean} Whether document is RTL. */ export function useRtl() { return getRtl(); } +/** + * Flips a CSS property from left <-> right. + * + * @param {string} key The CSS property name. + * + * @return {string} The flipped CSS property name, if applicable. + */ +function getConvertedKey( key ) { + if ( key === 'left' ) { + return 'right'; + } + + if ( key === 'right' ) { + return 'left'; + } + + if ( LOWER_LEFT_REGEXP.test( key ) ) { + return key.replace( LOWER_LEFT_REGEXP, '-right' ); + } + + if ( LOWER_RIGHT_REGEXP.test( key ) ) { + return key.replace( LOWER_RIGHT_REGEXP, '-left' ); + } + + if ( UPPER_LEFT_REGEXP.test( key ) ) { + return key.replace( UPPER_LEFT_REGEXP, 'Right' ); + } + + if ( UPPER_RIGHT_REGEXP.test( key ) ) { + return key.replace( UPPER_RIGHT_REGEXP, 'Left' ); + } + + return key; +} + /** * An incredibly basic ltr -> rtl converter for style properties * * @param {Object} ltrStyles + * * @return {Object} Converted ltr -> rtl styles */ -const convertLtrToRtl = ( ltrStyles = {} ) => { - const nextStyles = {}; - - for ( const key in ltrStyles ) { - const value = ltrStyles[ key ]; - let nextKey = key; - if ( /left/gi.test( key ) ) { - nextKey = [ key.replace( 'left', 'right' ) ]; - } - if ( /Left/gi.test( key ) ) { - nextKey = [ key.replace( 'Left', 'Right' ) ]; - } - nextStyles[ nextKey ] = value; - } - - return nextStyles; +export const convertLTRToRTL = ( ltrStyles = {} ) => { + return mapKeys( ltrStyles, ( _value, key ) => getConvertedKey( key ) ); }; /** - * An incredibly basic ltr -> rtl style converter for CSS objects. + * A higher-order function that create an incredibly basic ltr -> rtl style converter for CSS objects. * * @param {Object} ltrStyles Ltr styles. Converts and renders from ltr -> rtl styles, if applicable. * @param {null|Object} rtlStyles Rtl styles. Renders if provided. - * @return {Object} Rendered CSS styles for Emotion's renderer + * + * @return {Function} A function to output CSS styles for Emotion's renderer */ export function rtl( ltrStyles = {}, rtlStyles ) { return () => { @@ -53,6 +89,6 @@ export function rtl( ltrStyles = {}, rtlStyles ) { return isRtl ? css( rtlStyles ) : css( ltrStyles ); } - return isRtl ? css( convertLtrToRtl( ltrStyles ) ) : css( ltrStyles ); + return isRtl ? css( convertLTRToRTL( ltrStyles ) ) : css( ltrStyles ); }; } diff --git a/packages/components/src/utils/test/rtl.js b/packages/components/src/utils/test/rtl.js new file mode 100644 index 0000000000000..1e30162037087 --- /dev/null +++ b/packages/components/src/utils/test/rtl.js @@ -0,0 +1,124 @@ +/** + * Internal dependencies + */ +import { convertLTRToRTL } from '../rtl'; + +describe( 'convertLTRToRTL', () => { + it( 'converts (*)Left <-> (*)Right', () => { + const style = { + // left values + borderLeft: '10px solid red', + borderLeftColor: 'red', + borderLeftStyle: 'solid', + borderLeftWidth: 10, + borderTopLeftRadius: 10, + marginLeft: 10, + scrollMarginLeft: 10, + scrollPaddingLeft: 10, + // right values + paddingLeft: 10, + borderRight: '20px solid blue', + borderRightColor: 'blue', + borderRightStyle: 'dashed', + borderRightWidth: 20, + borderTopRightRadius: 20, + marginRight: 20, + paddingRight: 20, + scrollMarginRight: 20, + scrollPaddingRight: 20, + // edge cases + textCombineUpright: 'none', + }; + const nextStyle = convertLTRToRTL( style ); + + expect( Object.keys( style ).length ).toBe( + Object.keys( nextStyle ).length + ); + + // Left -> Right + expect( nextStyle.borderRight ).toBe( '10px solid red' ); + expect( nextStyle.borderRightColor ).toBe( 'red' ); + expect( nextStyle.borderRightStyle ).toBe( 'solid' ); + expect( nextStyle.borderRightWidth ).toBe( 10 ); + expect( nextStyle.borderTopRightRadius ).toBe( 10 ); + expect( nextStyle.marginRight ).toBe( 10 ); + expect( nextStyle.paddingRight ).toBe( 10 ); + expect( nextStyle.scrollMarginRight ).toBe( 10 ); + expect( nextStyle.scrollPaddingRight ).toBe( 10 ); + + // Right -> Left + expect( nextStyle.borderLeft ).toBe( '20px solid blue' ); + expect( nextStyle.borderLeftColor ).toBe( 'blue' ); + expect( nextStyle.borderLeftStyle ).toBe( 'dashed' ); + expect( nextStyle.borderLeftWidth ).toBe( 20 ); + expect( nextStyle.borderTopLeftRadius ).toBe( 20 ); + expect( nextStyle.marginLeft ).toBe( 20 ); + expect( nextStyle.paddingLeft ).toBe( 20 ); + expect( nextStyle.scrollMarginLeft ).toBe( 20 ); + expect( nextStyle.scrollPaddingLeft ).toBe( 20 ); + + // Edge cases + expect( nextStyle.textCombineUpright ).toBe( 'none' ); + } ); + + it( 'converts (*)left <-> (*)right', () => { + const style = { + // left values + 'border-left': '10px solid red', + 'border-left-color': 'red', + 'border-left-style': 'solid', + 'border-left-width': 10, + 'border-top-left-radius': 10, + 'margin-left': 10, + 'padding-left': 10, + 'scroll-margin-left': 10, + 'scroll-padding-left': 10, + left: 10, + // right values + 'border-right': '20px solid blue', + 'border-right-color': 'blue', + 'border-right-style': 'dashed', + 'border-right-width': 20, + 'border-top-right-radius': 20, + 'margin-right': 20, + 'padding-right': 20, + 'scroll-margin-right': 20, + 'scroll-padding-right': 20, + right: 20, + // edge cases + 'text-combine-upright': 'none', + }; + const nextStyle = convertLTRToRTL( style ); + + expect( Object.keys( style ).length ).toBe( + Object.keys( nextStyle ).length + ); + + // left -> right + expect( nextStyle[ 'border-right' ] ).toBe( '10px solid red' ); + expect( nextStyle[ 'border-right-color' ] ).toBe( 'red' ); + expect( nextStyle[ 'border-right-style' ] ).toBe( 'solid' ); + expect( nextStyle[ 'border-right-width' ] ).toBe( 10 ); + expect( nextStyle[ 'border-top-right-radius' ] ).toBe( 10 ); + expect( nextStyle[ 'margin-right' ] ).toBe( 10 ); + expect( nextStyle[ 'padding-right' ] ).toBe( 10 ); + expect( nextStyle[ 'scroll-margin-right' ] ).toBe( 10 ); + expect( nextStyle[ 'scroll-padding-right' ] ).toBe( 10 ); + expect( nextStyle.right ).toBe( 10 ); + + // right -> left + expect( nextStyle[ 'border-left' ] ).toBe( '20px solid blue' ); + expect( nextStyle[ 'border-left-color' ] ).toBe( 'blue' ); + expect( nextStyle[ 'border-left-style' ] ).toBe( 'dashed' ); + expect( nextStyle[ 'border-left-width' ] ).toBe( 20 ); + expect( nextStyle[ 'border-top-left-radius' ] ).toBe( 20 ); + expect( nextStyle[ 'margin-left' ] ).toBe( 20 ); + expect( nextStyle[ 'padding-left' ] ).toBe( 20 ); + expect( nextStyle[ 'scroll-margin-left' ] ).toBe( 20 ); + expect( nextStyle[ 'scroll-padding-left' ] ).toBe( 20 ); + expect( nextStyle.left ).toBe( 20 ); + + // Edge cases + expect( nextStyle[ 'text-combine-upright' ] ).toBe( 'none' ); + } ); +} );