Skip to content

Commit 51596f6

Browse files
committed
feat: add directional modifier support in tw/twStyle
Extend tw tagged templates and twStyle() to support ltr:/rtl: modifiers: - Generate ternary expressions using I18nManager.isRTL - Return ltrStyle/rtlStyle properties for manual access - Inject I18nManager import when directional modifiers detected - Add comprehensive tests for tw/twStyle with directional modifiers
1 parent ced2305 commit 51596f6

File tree

2 files changed

+243
-3
lines changed

2 files changed

+243
-3
lines changed

src/babel/plugin/visitors/tw.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,3 +618,154 @@ describe("tw/twStyle - integration with className", () => {
618618
expect(output).toContain("_m_4_p_2");
619619
});
620620
});
621+
622+
describe("tw visitor - directional modifiers (RTL/LTR)", () => {
623+
it("should transform rtl: modifier in tw template", () => {
624+
const input = `
625+
import { tw } from '@mgcrea/react-native-tailwind';
626+
627+
function MyComponent() {
628+
const styles = tw\`p-4 rtl:mr-4\`;
629+
return null;
630+
}
631+
`;
632+
633+
const output = transform(input);
634+
635+
// Should import I18nManager
636+
expect(output).toContain("I18nManager");
637+
638+
// Should declare _twIsRTL variable
639+
expect(output).toContain("_twIsRTL");
640+
expect(output).toContain("I18nManager.isRTL");
641+
642+
// Should have style array with conditional
643+
expect(output).toContain("style:");
644+
expect(output).toContain("_twStyles._p_4");
645+
expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
646+
647+
// Should have rtlStyle property
648+
expect(output).toContain("rtlStyle:");
649+
expect(output).toContain("_twStyles._rtl_mr_4");
650+
});
651+
652+
it("should transform ltr: modifier with negated conditional", () => {
653+
const input = `
654+
import { tw } from '@mgcrea/react-native-tailwind';
655+
656+
function MyComponent() {
657+
const styles = tw\`p-4 ltr:ml-4\`;
658+
return null;
659+
}
660+
`;
661+
662+
const output = transform(input);
663+
664+
// Should import I18nManager
665+
expect(output).toContain("I18nManager");
666+
667+
// Should have negated conditional for LTR (!_twIsRTL)
668+
expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
669+
670+
// Should have ltrStyle property
671+
expect(output).toContain("ltrStyle:");
672+
});
673+
674+
it("should combine rtl: and ltr: modifiers", () => {
675+
const input = `
676+
import { tw } from '@mgcrea/react-native-tailwind';
677+
678+
function MyComponent() {
679+
const styles = tw\`rtl:mr-4 ltr:ml-4\`;
680+
return null;
681+
}
682+
`;
683+
684+
const output = transform(input);
685+
686+
// Should have both conditionals
687+
expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
688+
expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
689+
690+
// Should have both style properties
691+
expect(output).toContain("rtlStyle:");
692+
expect(output).toContain("ltrStyle:");
693+
});
694+
695+
it("should combine directional modifiers with platform modifiers", () => {
696+
const input = `
697+
import { tw } from '@mgcrea/react-native-tailwind';
698+
699+
function MyComponent() {
700+
const styles = tw\`p-4 ios:p-6 rtl:mr-4\`;
701+
return null;
702+
}
703+
`;
704+
705+
const output = transform(input);
706+
707+
// Should have Platform import
708+
expect(output).toContain("Platform");
709+
710+
// Should have I18nManager import
711+
expect(output).toContain("I18nManager");
712+
713+
// Should have both modifiers in style array
714+
expect(output).toContain("Platform.select");
715+
expect(output).toMatch(/_twIsRTL\s*&&/);
716+
717+
// Should have iosStyle and rtlStyle properties
718+
expect(output).toContain("iosStyle:");
719+
expect(output).toContain("rtlStyle:");
720+
});
721+
722+
it("should combine directional modifiers with state modifiers", () => {
723+
const input = `
724+
import { tw } from '@mgcrea/react-native-tailwind';
725+
726+
function MyComponent() {
727+
const styles = tw\`bg-white active:bg-blue-500 rtl:pr-4\`;
728+
return null;
729+
}
730+
`;
731+
732+
const output = transform(input);
733+
734+
// Should have I18nManager import
735+
expect(output).toContain("I18nManager");
736+
737+
// Should have directional conditional
738+
expect(output).toMatch(/_twIsRTL\s*&&/);
739+
740+
// Should have activeStyle property
741+
expect(output).toContain("activeStyle:");
742+
expect(output).toContain("_twStyles._active_bg_blue_500");
743+
744+
// Should have rtlStyle property
745+
expect(output).toContain("rtlStyle:");
746+
});
747+
748+
it("should work with twStyle function for RTL modifiers", () => {
749+
const input = `
750+
import { twStyle } from '@mgcrea/react-native-tailwind';
751+
752+
function MyComponent() {
753+
const styles = twStyle('p-4 rtl:mr-4 ltr:ml-4');
754+
return null;
755+
}
756+
`;
757+
758+
const output = transform(input);
759+
760+
// Should import I18nManager
761+
expect(output).toContain("I18nManager");
762+
763+
// Should have both conditionals
764+
expect(output).toMatch(/_twIsRTL\s*&&/);
765+
expect(output).toMatch(/!\s*_twIsRTL\s*&&/);
766+
767+
// Should have both style properties
768+
expect(output).toContain("rtlStyle:");
769+
expect(output).toContain("ltrStyle:");
770+
});
771+
});

src/babel/utils/twProcessing.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import type { CustomTheme, ModifierType, ParsedModifier } from "../../parser/ind
88
import {
99
expandSchemeModifier,
1010
isColorSchemeModifier,
11+
isDirectionalModifier,
1112
isPlatformModifier,
1213
isSchemeModifier,
1314
} from "../../parser/index.js";
1415
import type { SchemeModifierConfig } from "../../types/config.js";
1516
import type { StyleObject } from "../../types/core.js";
1617
import { processColorSchemeModifiers } from "./colorSchemeModifierProcessing.js";
18+
import { processDirectionalModifiers } from "./directionalModifierProcessing.js";
1719
import { processPlatformModifiers } from "./platformModifierProcessing.js";
1820
import { hasRuntimeDimensions } from "./windowDimensionsProcessing.js";
1921

@@ -33,6 +35,9 @@ export interface TwProcessingState {
3335
colorSchemeLocalIdentifier?: string;
3436
// Platform support (for ios:/android:/web: modifiers)
3537
needsPlatformImport: boolean;
38+
// Directional support (for rtl:/ltr: modifiers)
39+
needsI18nManagerImport: boolean;
40+
i18nManagerVariableName: string;
3641
}
3742

3843
/**
@@ -102,11 +107,15 @@ export function processTwCall(
102107
objectProperties.push(t.objectProperty(t.identifier("style"), t.objectExpression([])));
103108
}
104109

105-
// Separate color-scheme and platform modifiers from other modifiers
110+
// Separate color-scheme, platform, and directional modifiers from other modifiers
106111
const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
107112
const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
113+
const directionalModifiers = modifierClasses.filter((m) => isDirectionalModifier(m.modifier));
108114
const otherModifiers = modifierClasses.filter(
109-
(m) => !isColorSchemeModifier(m.modifier) && !isPlatformModifier(m.modifier),
115+
(m) =>
116+
!isColorSchemeModifier(m.modifier) &&
117+
!isPlatformModifier(m.modifier) &&
118+
!isDirectionalModifier(m.modifier),
110119
);
111120

112121
// Check if we need color scheme support
@@ -293,7 +302,87 @@ export function processTwCall(
293302
}
294303
}
295304

296-
// Group other modifiers by type (non-color-scheme and non-platform modifiers)
305+
// Process directional modifiers if present
306+
const hasDirectionalModifiers = directionalModifiers.length > 0;
307+
308+
if (hasDirectionalModifiers) {
309+
// Mark that we need I18nManager import
310+
state.needsI18nManagerImport = true;
311+
312+
// Generate directional conditional expressions
313+
const directionalConditionals = processDirectionalModifiers(
314+
directionalModifiers,
315+
state,
316+
parseClassName,
317+
generateStyleKey,
318+
t,
319+
);
320+
321+
// If we already have a style array (from color scheme or platform modifiers), add to it
322+
// Otherwise, convert style property to an array
323+
const styleProperty = objectProperties.find(
324+
(prop) => t.isIdentifier(prop.key) && prop.key.name === "style",
325+
);
326+
327+
if (styleProperty && t.isArrayExpression(styleProperty.value)) {
328+
// Already have style array, add directional conditionals to it
329+
styleProperty.value.elements.push(...directionalConditionals);
330+
} else {
331+
// No existing array, create style array with base + directional conditionals
332+
const styleArrayElements: BabelTypes.Expression[] = [];
333+
334+
// Add base style if present
335+
if (baseClasses.length > 0) {
336+
const baseClassName = baseClasses.join(" ");
337+
const baseStyleObject = parseClassName(baseClassName, state.customTheme);
338+
const baseStyleKey = generateStyleKey(baseClassName);
339+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
340+
styleArrayElements.push(
341+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
342+
);
343+
}
344+
345+
// Add directional conditionals
346+
styleArrayElements.push(...directionalConditionals);
347+
348+
// Replace style property with array
349+
objectProperties[0] = t.objectProperty(t.identifier("style"), t.arrayExpression(styleArrayElements));
350+
}
351+
352+
// Also add rtlStyle/ltrStyle properties for manual processing
353+
const rtlModifiers = directionalModifiers.filter((m) => m.modifier === "rtl");
354+
const ltrModifiers = directionalModifiers.filter((m) => m.modifier === "ltr");
355+
356+
if (rtlModifiers.length > 0) {
357+
const rtlClassNames = rtlModifiers.map((m) => m.baseClass).join(" ");
358+
const rtlStyleObject = parseClassName(rtlClassNames, state.customTheme);
359+
const rtlStyleKey = generateStyleKey(`rtl_${rtlClassNames}`);
360+
state.styleRegistry.set(rtlStyleKey, rtlStyleObject);
361+
362+
objectProperties.push(
363+
t.objectProperty(
364+
t.identifier("rtlStyle"),
365+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(rtlStyleKey)),
366+
),
367+
);
368+
}
369+
370+
if (ltrModifiers.length > 0) {
371+
const ltrClassNames = ltrModifiers.map((m) => m.baseClass).join(" ");
372+
const ltrStyleObject = parseClassName(ltrClassNames, state.customTheme);
373+
const ltrStyleKey = generateStyleKey(`ltr_${ltrClassNames}`);
374+
state.styleRegistry.set(ltrStyleKey, ltrStyleObject);
375+
376+
objectProperties.push(
377+
t.objectProperty(
378+
t.identifier("ltrStyle"),
379+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(ltrStyleKey)),
380+
),
381+
);
382+
}
383+
}
384+
385+
// Group other modifiers by type (non-color-scheme, non-platform, and non-directional modifiers)
297386
const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
298387
for (const mod of otherModifiers) {
299388
if (!modifiersByType.has(mod.modifier)) {

0 commit comments

Comments
 (0)