@mgcrea/react-native-tailwind 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +459 -39
- package/dist/babel/index.cjs +810 -279
- package/dist/babel/index.d.ts +2 -1
- package/dist/babel/index.ts +328 -22
- package/dist/components/Pressable.d.ts +32 -0
- package/dist/components/Pressable.js +1 -0
- package/dist/components/TextInput.d.ts +56 -0
- package/dist/components/TextInput.js +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1 -1
- package/dist/parser/aspectRatio.d.ts +16 -0
- package/dist/parser/aspectRatio.js +1 -0
- package/dist/parser/aspectRatio.test.d.ts +1 -0
- package/dist/parser/aspectRatio.test.js +1 -0
- package/dist/parser/borders.js +1 -1
- package/dist/parser/borders.test.d.ts +1 -0
- package/dist/parser/borders.test.js +1 -0
- package/dist/parser/colors.d.ts +1 -0
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.d.ts +1 -0
- package/dist/parser/colors.test.js +1 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.d.ts +2 -0
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.d.ts +1 -0
- package/dist/parser/layout.test.js +1 -0
- package/dist/parser/modifiers.d.ts +47 -0
- package/dist/parser/modifiers.js +1 -0
- package/dist/parser/modifiers.test.d.ts +1 -0
- package/dist/parser/modifiers.test.js +1 -0
- package/dist/parser/shadows.d.ts +26 -0
- package/dist/parser/shadows.js +1 -0
- package/dist/parser/shadows.test.d.ts +1 -0
- package/dist/parser/shadows.test.js +1 -0
- package/dist/parser/sizing.test.d.ts +1 -0
- package/dist/parser/sizing.test.js +1 -0
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.d.ts +1 -0
- package/dist/parser/spacing.test.js +1 -0
- package/dist/parser/typography.d.ts +2 -1
- package/dist/parser/typography.js +1 -1
- package/dist/parser/typography.test.d.ts +1 -0
- package/dist/parser/typography.test.js +1 -0
- package/dist/types.d.ts +5 -2
- package/package.json +7 -6
- package/src/babel/index.ts +328 -22
- package/src/components/Pressable.tsx +46 -0
- package/src/components/TextInput.tsx +90 -0
- package/src/index.ts +20 -2
- package/src/parser/aspectRatio.test.ts +191 -0
- package/src/parser/aspectRatio.ts +73 -0
- package/src/parser/borders.test.ts +329 -0
- package/src/parser/borders.ts +187 -108
- package/src/parser/colors.test.ts +335 -0
- package/src/parser/colors.ts +117 -6
- package/src/parser/index.ts +13 -2
- package/src/parser/layout.test.ts +459 -0
- package/src/parser/layout.ts +128 -0
- package/src/parser/modifiers.test.ts +375 -0
- package/src/parser/modifiers.ts +104 -0
- package/src/parser/shadows.test.ts +201 -0
- package/src/parser/shadows.ts +133 -0
- package/src/parser/sizing.test.ts +256 -0
- package/src/parser/spacing.test.ts +226 -0
- package/src/parser/spacing.ts +93 -138
- package/src/parser/typography.test.ts +221 -0
- package/src/parser/typography.ts +143 -112
- package/src/types.ts +2 -2
- package/dist/react-native.d.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var _vitest=require("vitest");var _typography=require("./typography");(0,_vitest.describe)("FONT_SIZES",function(){(0,_vitest.it)("should export complete font size scale",function(){(0,_vitest.expect)(_typography.FONT_SIZES).toMatchSnapshot();});});(0,_vitest.describe)("LETTER_SPACING_SCALE",function(){(0,_vitest.it)("should export complete letter spacing scale",function(){(0,_vitest.expect)(_typography.LETTER_SPACING_SCALE).toMatchSnapshot();});});(0,_vitest.describe)("parseTypography - font size",function(){(0,_vitest.it)("should parse font size with preset values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-xs")).toEqual({fontSize:12});(0,_vitest.expect)((0,_typography.parseTypography)("text-sm")).toEqual({fontSize:14});(0,_vitest.expect)((0,_typography.parseTypography)("text-base")).toEqual({fontSize:16});(0,_vitest.expect)((0,_typography.parseTypography)("text-lg")).toEqual({fontSize:18});(0,_vitest.expect)((0,_typography.parseTypography)("text-xl")).toEqual({fontSize:20});(0,_vitest.expect)((0,_typography.parseTypography)("text-2xl")).toEqual({fontSize:24});(0,_vitest.expect)((0,_typography.parseTypography)("text-3xl")).toEqual({fontSize:30});(0,_vitest.expect)((0,_typography.parseTypography)("text-4xl")).toEqual({fontSize:36});(0,_vitest.expect)((0,_typography.parseTypography)("text-5xl")).toEqual({fontSize:48});(0,_vitest.expect)((0,_typography.parseTypography)("text-6xl")).toEqual({fontSize:60});(0,_vitest.expect)((0,_typography.parseTypography)("text-7xl")).toEqual({fontSize:72});(0,_vitest.expect)((0,_typography.parseTypography)("text-8xl")).toEqual({fontSize:96});(0,_vitest.expect)((0,_typography.parseTypography)("text-9xl")).toEqual({fontSize:128});});(0,_vitest.it)("should parse font size with arbitrary pixel values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-[18px]")).toEqual({fontSize:18});(0,_vitest.expect)((0,_typography.parseTypography)("text-[18]")).toEqual({fontSize:18});(0,_vitest.expect)((0,_typography.parseTypography)("text-[22px]")).toEqual({fontSize:22});(0,_vitest.expect)((0,_typography.parseTypography)("text-[22]")).toEqual({fontSize:22});(0,_vitest.expect)((0,_typography.parseTypography)("text-[100px]")).toEqual({fontSize:100});});});(0,_vitest.describe)("parseTypography - font weight",function(){(0,_vitest.it)("should parse font weight values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("font-thin")).toEqual({fontWeight:"100"});(0,_vitest.expect)((0,_typography.parseTypography)("font-extralight")).toEqual({fontWeight:"200"});(0,_vitest.expect)((0,_typography.parseTypography)("font-light")).toEqual({fontWeight:"300"});(0,_vitest.expect)((0,_typography.parseTypography)("font-normal")).toEqual({fontWeight:"400"});(0,_vitest.expect)((0,_typography.parseTypography)("font-medium")).toEqual({fontWeight:"500"});(0,_vitest.expect)((0,_typography.parseTypography)("font-semibold")).toEqual({fontWeight:"600"});(0,_vitest.expect)((0,_typography.parseTypography)("font-bold")).toEqual({fontWeight:"700"});(0,_vitest.expect)((0,_typography.parseTypography)("font-extrabold")).toEqual({fontWeight:"800"});(0,_vitest.expect)((0,_typography.parseTypography)("font-black")).toEqual({fontWeight:"900"});});});(0,_vitest.describe)("parseTypography - font style",function(){(0,_vitest.it)("should parse font style values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("italic")).toEqual({fontStyle:"italic"});(0,_vitest.expect)((0,_typography.parseTypography)("not-italic")).toEqual({fontStyle:"normal"});});});(0,_vitest.describe)("parseTypography - text alignment",function(){(0,_vitest.it)("should parse text alignment values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-left")).toEqual({textAlign:"left"});(0,_vitest.expect)((0,_typography.parseTypography)("text-center")).toEqual({textAlign:"center"});(0,_vitest.expect)((0,_typography.parseTypography)("text-right")).toEqual({textAlign:"right"});(0,_vitest.expect)((0,_typography.parseTypography)("text-justify")).toEqual({textAlign:"justify"});});});(0,_vitest.describe)("parseTypography - text decoration",function(){(0,_vitest.it)("should parse text decoration values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("underline")).toEqual({textDecorationLine:"underline"});(0,_vitest.expect)((0,_typography.parseTypography)("line-through")).toEqual({textDecorationLine:"line-through"});(0,_vitest.expect)((0,_typography.parseTypography)("no-underline")).toEqual({textDecorationLine:"none"});});});(0,_vitest.describe)("parseTypography - text transform",function(){(0,_vitest.it)("should parse text transform values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("uppercase")).toEqual({textTransform:"uppercase"});(0,_vitest.expect)((0,_typography.parseTypography)("lowercase")).toEqual({textTransform:"lowercase"});(0,_vitest.expect)((0,_typography.parseTypography)("capitalize")).toEqual({textTransform:"capitalize"});(0,_vitest.expect)((0,_typography.parseTypography)("normal-case")).toEqual({textTransform:"none"});});});(0,_vitest.describe)("parseTypography - line height",function(){(0,_vitest.it)("should parse line height with preset values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("leading-none")).toEqual({lineHeight:16});(0,_vitest.expect)((0,_typography.parseTypography)("leading-tight")).toEqual({lineHeight:20});(0,_vitest.expect)((0,_typography.parseTypography)("leading-snug")).toEqual({lineHeight:22});(0,_vitest.expect)((0,_typography.parseTypography)("leading-normal")).toEqual({lineHeight:24});(0,_vitest.expect)((0,_typography.parseTypography)("leading-relaxed")).toEqual({lineHeight:28});(0,_vitest.expect)((0,_typography.parseTypography)("leading-loose")).toEqual({lineHeight:32});});(0,_vitest.it)("should parse line height with arbitrary pixel values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("leading-[24px]")).toEqual({lineHeight:24});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[24]")).toEqual({lineHeight:24});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[30px]")).toEqual({lineHeight:30});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[30]")).toEqual({lineHeight:30});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[50px]")).toEqual({lineHeight:50});});});(0,_vitest.describe)("parseTypography - letter spacing",function(){(0,_vitest.it)("should parse letter spacing with preset values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("tracking-tighter")).toEqual({letterSpacing:-0.8});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-tight")).toEqual({letterSpacing:-0.4});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-normal")).toEqual({letterSpacing:0});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-wide")).toEqual({letterSpacing:0.4});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-wider")).toEqual({letterSpacing:0.8});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-widest")).toEqual({letterSpacing:1.6});});});(0,_vitest.describe)("parseTypography - edge cases",function(){(0,_vitest.it)("should return null for invalid classes",function(){(0,_vitest.expect)((0,_typography.parseTypography)("invalid")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("font")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("tracking")).toBeNull();});(0,_vitest.it)("should return null for invalid font size values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-invalid")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text-10xl")).toBeNull();});(0,_vitest.it)("should return null for invalid font weight values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("font-invalid")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("font-100")).toBeNull();});(0,_vitest.it)("should return null for arbitrary values with unsupported units",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-[16rem]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text-[2em]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading-[2rem]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading-[1.5em]")).toBeNull();});(0,_vitest.it)("should return null for malformed arbitrary values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-[16")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text-16]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text-[]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading-[24")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading-24]")).toBeNull();});(0,_vitest.it)("should not match partial class names",function(){(0,_vitest.expect)((0,_typography.parseTypography)("mytext-lg")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("font-bold-extra")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("italic-text")).toBeNull();});});(0,_vitest.describe)("parseTypography - comprehensive coverage",function(){(0,_vitest.it)("should handle all typography categories independently",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-base")).toEqual({fontSize:16});(0,_vitest.expect)((0,_typography.parseTypography)("font-bold")).toEqual({fontWeight:"700"});(0,_vitest.expect)((0,_typography.parseTypography)("italic")).toEqual({fontStyle:"italic"});(0,_vitest.expect)((0,_typography.parseTypography)("text-center")).toEqual({textAlign:"center"});(0,_vitest.expect)((0,_typography.parseTypography)("underline")).toEqual({textDecorationLine:"underline"});(0,_vitest.expect)((0,_typography.parseTypography)("uppercase")).toEqual({textTransform:"uppercase"});(0,_vitest.expect)((0,_typography.parseTypography)("leading-normal")).toEqual({lineHeight:24});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-wide")).toEqual({letterSpacing:0.4});});(0,_vitest.it)("should handle arbitrary values for font size and line height",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-[19px]")).toEqual({fontSize:19});(0,_vitest.expect)((0,_typography.parseTypography)("text-[19]")).toEqual({fontSize:19});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[26px]")).toEqual({lineHeight:26});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[26]")).toEqual({lineHeight:26});});(0,_vitest.it)("should handle edge case values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-xs")).toEqual({fontSize:12});(0,_vitest.expect)((0,_typography.parseTypography)("text-9xl")).toEqual({fontSize:128});(0,_vitest.expect)((0,_typography.parseTypography)("leading-none")).toEqual({lineHeight:16});(0,_vitest.expect)((0,_typography.parseTypography)("leading-loose")).toEqual({lineHeight:32});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-tighter")).toEqual({letterSpacing:-0.8});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-widest")).toEqual({letterSpacing:1.6});});});
|
package/dist/types.d.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ImageStyle, TextStyle, ViewStyle } from "react-native";
|
|
5
5
|
export type RNStyle = ViewStyle | TextStyle | ImageStyle;
|
|
6
|
-
export type StyleObject = {
|
|
7
|
-
|
|
6
|
+
export type StyleObject = Record<Exclude<string, "shadowOffset">, string | number | undefined> & {
|
|
7
|
+
shadowOffset?: {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
8
11
|
};
|
|
9
12
|
export type SpacingValue = number;
|
|
10
13
|
export type ColorValue = string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgcrea/react-native-tailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
|
|
5
5
|
"author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
|
|
6
6
|
"homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
"dev": "cd example; npm run dev",
|
|
34
34
|
"build": "npm run build:babel && npm run build:babel-plugin && npm run build:types",
|
|
35
35
|
"build:babel": "babel src --out-dir dist --extensions \".ts,.tsx,.js,.jsx\" --copy-files --ignore 'src/babel/**' --ignore '**/*.d.ts'",
|
|
36
|
-
"build:babel-plugin": "node scripts/bundle-babel-plugin.
|
|
37
|
-
"build:types": "tsc --emitDeclarationOnly && node scripts/post-build-types.
|
|
36
|
+
"build:babel-plugin": "node --experimental-strip-types scripts/bundle-babel-plugin.ts",
|
|
37
|
+
"build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly && node --experimental-strip-types scripts/post-build-types.ts",
|
|
38
38
|
"install:ios": "cd example; npm run install:ios",
|
|
39
39
|
"open:ios": "cd example; npm run open:ios",
|
|
40
|
-
"lint": "eslint
|
|
40
|
+
"lint": "eslint src/",
|
|
41
41
|
"prettify": "prettier --write src/",
|
|
42
42
|
"check": "tsc --noEmit",
|
|
43
|
-
"spec": "
|
|
43
|
+
"spec": "vitest",
|
|
44
44
|
"test": "npm run lint && npm run check && npm run spec",
|
|
45
45
|
"prepare": "npm run build"
|
|
46
46
|
},
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
72
72
|
"react": "^19.2.0",
|
|
73
73
|
"react-native": "0.82.1",
|
|
74
|
-
"typescript": "^5.9.3"
|
|
74
|
+
"typescript": "^5.9.3",
|
|
75
|
+
"vitest": "^4.0.10"
|
|
75
76
|
},
|
|
76
77
|
"engines": {
|
|
77
78
|
"node": ">=18"
|
package/src/babel/index.ts
CHANGED
|
@@ -11,22 +11,28 @@
|
|
|
11
11
|
|
|
12
12
|
import type { NodePath, PluginObj, PluginPass } from "@babel/core";
|
|
13
13
|
import * as BabelTypes from "@babel/types";
|
|
14
|
-
import {
|
|
14
|
+
import { StyleObject } from "src/types.js";
|
|
15
|
+
import type { ModifierType, ParsedModifier } from "../parser/index.js";
|
|
16
|
+
import { parseClassName as parseClassNameFn, splitModifierClasses } from "../parser/index.js";
|
|
15
17
|
import { generateStyleKey as generateStyleKeyFn } from "../utils/styleKey.js";
|
|
16
18
|
import { extractCustomColors } from "./config-loader.js";
|
|
17
19
|
|
|
18
20
|
type PluginState = PluginPass & {
|
|
19
|
-
styleRegistry: Map<string,
|
|
21
|
+
styleRegistry: Map<string, StyleObject>;
|
|
20
22
|
hasClassNames: boolean;
|
|
21
23
|
hasStyleSheetImport: boolean;
|
|
22
24
|
customColors: Record<string, string>;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
// Use a unique identifier to avoid conflicts with user's own styles
|
|
28
|
+
const STYLES_IDENTIFIER = "_twStyles";
|
|
29
|
+
|
|
25
30
|
/**
|
|
26
31
|
* Supported className-like attributes
|
|
27
32
|
*/
|
|
28
33
|
const SUPPORTED_CLASS_ATTRIBUTES = [
|
|
29
34
|
"className",
|
|
35
|
+
"containerClassName",
|
|
30
36
|
"contentContainerClassName",
|
|
31
37
|
"columnWrapperClassName",
|
|
32
38
|
"ListHeaderComponentClassName",
|
|
@@ -37,6 +43,9 @@ const SUPPORTED_CLASS_ATTRIBUTES = [
|
|
|
37
43
|
* Get the target style prop name based on the className attribute
|
|
38
44
|
*/
|
|
39
45
|
function getTargetStyleProp(attributeName: string): string {
|
|
46
|
+
if (attributeName === "containerClassName") {
|
|
47
|
+
return "containerStyle";
|
|
48
|
+
}
|
|
40
49
|
if (attributeName === "contentContainerClassName") {
|
|
41
50
|
return "contentContainerStyle";
|
|
42
51
|
}
|
|
@@ -52,6 +61,49 @@ function getTargetStyleProp(attributeName: string): string {
|
|
|
52
61
|
return "style";
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Check if a JSX element supports modifiers and determine which modifiers are supported
|
|
66
|
+
* Returns an object with component info and supported modifiers
|
|
67
|
+
*/
|
|
68
|
+
function getComponentModifierSupport(
|
|
69
|
+
jsxElement: any,
|
|
70
|
+
t: typeof BabelTypes,
|
|
71
|
+
): { component: string; supportedModifiers: ModifierType[] } | null {
|
|
72
|
+
if (!t.isJSXOpeningElement(jsxElement)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const name = jsxElement.name;
|
|
77
|
+
let componentName: string | null = null;
|
|
78
|
+
|
|
79
|
+
// Handle simple identifier: <Pressable>
|
|
80
|
+
if (t.isJSXIdentifier(name)) {
|
|
81
|
+
componentName = name.name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle member expression: <ReactNative.Pressable>
|
|
85
|
+
if (t.isJSXMemberExpression(name)) {
|
|
86
|
+
const property = name.property;
|
|
87
|
+
if (t.isJSXIdentifier(property)) {
|
|
88
|
+
componentName = property.name;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!componentName) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Map components to their supported modifiers
|
|
97
|
+
switch (componentName) {
|
|
98
|
+
case "Pressable":
|
|
99
|
+
return { component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"] };
|
|
100
|
+
case "TextInput":
|
|
101
|
+
return { component: "TextInput", supportedModifiers: ["focus", "disabled"] };
|
|
102
|
+
default:
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
55
107
|
/**
|
|
56
108
|
* Result of processing a dynamic expression
|
|
57
109
|
*/
|
|
@@ -117,7 +169,7 @@ function processTemplateLiteral(
|
|
|
117
169
|
staticParts.push(cls);
|
|
118
170
|
|
|
119
171
|
// Add to parts array
|
|
120
|
-
parts.push(t.memberExpression(t.identifier(
|
|
172
|
+
parts.push(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)));
|
|
121
173
|
}
|
|
122
174
|
}
|
|
123
175
|
|
|
@@ -216,7 +268,7 @@ function processStringOrExpression(node: any, state: PluginState, t: typeof Babe
|
|
|
216
268
|
const styleKey = generateStyleKey(className);
|
|
217
269
|
state.styleRegistry.set(styleKey, styleObject);
|
|
218
270
|
|
|
219
|
-
return t.memberExpression(t.identifier(
|
|
271
|
+
return t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey));
|
|
220
272
|
}
|
|
221
273
|
|
|
222
274
|
// Handle nested expressions recursively
|
|
@@ -239,6 +291,118 @@ function processStringOrExpression(node: any, state: PluginState, t: typeof Babe
|
|
|
239
291
|
return null;
|
|
240
292
|
}
|
|
241
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Process a static className string that contains modifiers
|
|
296
|
+
* Returns a style function expression for Pressable components
|
|
297
|
+
*/
|
|
298
|
+
function processStaticClassNameWithModifiers(
|
|
299
|
+
className: string,
|
|
300
|
+
state: PluginState,
|
|
301
|
+
t: typeof BabelTypes,
|
|
302
|
+
): any {
|
|
303
|
+
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
304
|
+
|
|
305
|
+
// Parse and register base classes
|
|
306
|
+
let baseStyleExpression: any = null;
|
|
307
|
+
if (baseClasses.length > 0) {
|
|
308
|
+
const baseClassName = baseClasses.join(" ");
|
|
309
|
+
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
310
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
311
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
312
|
+
baseStyleExpression = t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(baseStyleKey));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse and register modifier classes
|
|
316
|
+
// Group by modifier type for better organization
|
|
317
|
+
const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
|
|
318
|
+
for (const mod of modifierClasses) {
|
|
319
|
+
if (!modifiersByType.has(mod.modifier)) {
|
|
320
|
+
modifiersByType.set(mod.modifier, []);
|
|
321
|
+
}
|
|
322
|
+
const modGroup = modifiersByType.get(mod.modifier);
|
|
323
|
+
if (modGroup) {
|
|
324
|
+
modGroup.push(mod);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Build style function: ({ pressed }) => [baseStyle, pressed && modifierStyle]
|
|
329
|
+
const styleArrayElements: any[] = [];
|
|
330
|
+
|
|
331
|
+
// Add base style first
|
|
332
|
+
if (baseStyleExpression) {
|
|
333
|
+
styleArrayElements.push(baseStyleExpression);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Add conditional styles for each modifier type
|
|
337
|
+
for (const [modifierType, modifiers] of modifiersByType) {
|
|
338
|
+
// Parse all modifier classes together
|
|
339
|
+
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
340
|
+
const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
|
|
341
|
+
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
342
|
+
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
343
|
+
|
|
344
|
+
// Create conditional: pressed && styles._active_bg_blue_700
|
|
345
|
+
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
346
|
+
const conditionalExpression = t.logicalExpression(
|
|
347
|
+
"&&",
|
|
348
|
+
t.identifier(stateProperty),
|
|
349
|
+
t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(modifierStyleKey)),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
styleArrayElements.push(conditionalExpression);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If only base style, return it directly; otherwise return array
|
|
356
|
+
if (styleArrayElements.length === 1) {
|
|
357
|
+
return styleArrayElements[0];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return t.arrayExpression(styleArrayElements);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get the state property name for a modifier type
|
|
365
|
+
* Maps modifier types to component state parameter properties
|
|
366
|
+
*/
|
|
367
|
+
function getStatePropertyForModifier(modifier: ModifierType): string {
|
|
368
|
+
switch (modifier) {
|
|
369
|
+
case "active":
|
|
370
|
+
return "pressed";
|
|
371
|
+
case "hover":
|
|
372
|
+
return "hovered";
|
|
373
|
+
case "focus":
|
|
374
|
+
return "focused";
|
|
375
|
+
case "disabled":
|
|
376
|
+
return "disabled";
|
|
377
|
+
default:
|
|
378
|
+
return "pressed"; // fallback
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Create a style function for Pressable: ({ pressed }) => styleExpression
|
|
384
|
+
*/
|
|
385
|
+
function createStyleFunction(styleExpression: any, modifierTypes: ModifierType[], t: typeof BabelTypes): any {
|
|
386
|
+
// Build parameter object: { pressed, hovered, focused }
|
|
387
|
+
const paramProperties: any[] = [];
|
|
388
|
+
const usedStateProps = new Set<string>();
|
|
389
|
+
|
|
390
|
+
for (const modifierType of modifierTypes) {
|
|
391
|
+
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
392
|
+
if (!usedStateProps.has(stateProperty)) {
|
|
393
|
+
usedStateProps.add(stateProperty);
|
|
394
|
+
paramProperties.push(
|
|
395
|
+
t.objectProperty(t.identifier(stateProperty), t.identifier(stateProperty), false, true),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const param = t.objectPattern(paramProperties);
|
|
401
|
+
|
|
402
|
+
// Create arrow function: ({ pressed }) => styleExpression
|
|
403
|
+
return t.arrowFunctionExpression([param], styleExpression);
|
|
404
|
+
}
|
|
405
|
+
|
|
242
406
|
export default function reactNativeTailwindBabelPlugin({
|
|
243
407
|
types: t,
|
|
244
408
|
}: {
|
|
@@ -311,7 +475,7 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
311
475
|
// Determine target style prop based on attribute name
|
|
312
476
|
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
313
477
|
|
|
314
|
-
// Handle static string literals
|
|
478
|
+
// Handle static string literals
|
|
315
479
|
if (t.isStringLiteral(value)) {
|
|
316
480
|
const className = value.value.trim();
|
|
317
481
|
|
|
@@ -323,13 +487,99 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
323
487
|
|
|
324
488
|
state.hasClassNames = true;
|
|
325
489
|
|
|
326
|
-
//
|
|
327
|
-
const
|
|
490
|
+
// Check if className contains modifiers (active:, hover:, focus:)
|
|
491
|
+
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
328
492
|
|
|
329
|
-
//
|
|
330
|
-
|
|
493
|
+
// If there are modifiers, check if this component supports them
|
|
494
|
+
if (modifierClasses.length > 0) {
|
|
495
|
+
// Get the JSX opening element (the direct parent of the attribute)
|
|
496
|
+
const jsxOpeningElement = path.parent;
|
|
497
|
+
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
498
|
+
|
|
499
|
+
if (componentSupport) {
|
|
500
|
+
// Get modifier types used in className
|
|
501
|
+
const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
|
|
502
|
+
|
|
503
|
+
// Check if all modifiers are supported by this component
|
|
504
|
+
const unsupportedModifiers = usedModifiers.filter(
|
|
505
|
+
(mod) => !componentSupport.supportedModifiers.includes(mod),
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (unsupportedModifiers.length > 0) {
|
|
509
|
+
// Warn about unsupported modifiers
|
|
510
|
+
if (process.env.NODE_ENV !== "production") {
|
|
511
|
+
console.warn(
|
|
512
|
+
`[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
513
|
+
`Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
// Filter out unsupported modifiers
|
|
517
|
+
const supportedModifierClasses = modifierClasses.filter((m) =>
|
|
518
|
+
componentSupport.supportedModifiers.includes(m.modifier),
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// If no supported modifiers remain, fall through to normal processing
|
|
522
|
+
if (supportedModifierClasses.length === 0) {
|
|
523
|
+
// Continue to normal processing
|
|
524
|
+
} else {
|
|
525
|
+
// Process only supported modifiers
|
|
526
|
+
const filteredClassName =
|
|
527
|
+
baseClasses.join(" ") +
|
|
528
|
+
" " +
|
|
529
|
+
supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
|
|
530
|
+
const styleExpression = processStaticClassNameWithModifiers(
|
|
531
|
+
filteredClassName.trim(),
|
|
532
|
+
state,
|
|
533
|
+
t,
|
|
534
|
+
);
|
|
535
|
+
const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
|
|
536
|
+
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
537
|
+
|
|
538
|
+
const parent = path.parent as any;
|
|
539
|
+
const styleAttribute = parent.attributes.find(
|
|
540
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
if (styleAttribute) {
|
|
544
|
+
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
545
|
+
} else {
|
|
546
|
+
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
// All modifiers are supported - process normally
|
|
552
|
+
const styleExpression = processStaticClassNameWithModifiers(className, state, t);
|
|
553
|
+
const modifierTypes = usedModifiers;
|
|
554
|
+
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
555
|
+
|
|
556
|
+
const parent = path.parent as any;
|
|
557
|
+
const styleAttribute = parent.attributes.find(
|
|
558
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
if (styleAttribute) {
|
|
562
|
+
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
563
|
+
} else {
|
|
564
|
+
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
// Component doesn't support any modifiers
|
|
570
|
+
if (process.env.NODE_ENV !== "production") {
|
|
571
|
+
const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
|
|
572
|
+
console.warn(
|
|
573
|
+
`[react-native-tailwind] Modifiers (${usedModifiers.map((m) => `${m}:`).join(", ")}) can only be used on compatible components (Pressable, TextInput). Found on unsupported element at ${state.file.opts.filename ?? "unknown"}`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
// Fall through to normal processing (ignore modifiers)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
331
579
|
|
|
332
|
-
//
|
|
580
|
+
// Normal processing without modifiers
|
|
581
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
582
|
+
const styleKey = generateStyleKey(className);
|
|
333
583
|
state.styleRegistry.set(styleKey, styleObject);
|
|
334
584
|
|
|
335
585
|
// Check if there's already a style prop on this element
|
|
@@ -426,7 +676,7 @@ function replaceWithStyleAttribute(
|
|
|
426
676
|
) {
|
|
427
677
|
const styleAttribute = t.jsxAttribute(
|
|
428
678
|
t.jsxIdentifier(targetStyleProp),
|
|
429
|
-
t.jsxExpressionContainer(t.memberExpression(t.identifier(
|
|
679
|
+
t.jsxExpressionContainer(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey))),
|
|
430
680
|
);
|
|
431
681
|
|
|
432
682
|
classNamePath.replaceWith(styleAttribute);
|
|
@@ -446,7 +696,7 @@ function mergeStyleAttribute(
|
|
|
446
696
|
// Create array with className styles first, then existing styles
|
|
447
697
|
// This allows existing styles to override className styles
|
|
448
698
|
const styleArray = t.arrayExpression([
|
|
449
|
-
t.memberExpression(t.identifier(
|
|
699
|
+
t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)),
|
|
450
700
|
existingStyle,
|
|
451
701
|
]);
|
|
452
702
|
|
|
@@ -502,13 +752,72 @@ function mergeDynamicStyleAttribute(
|
|
|
502
752
|
}
|
|
503
753
|
|
|
504
754
|
/**
|
|
505
|
-
*
|
|
755
|
+
* Replace className with style function attribute (for Pressable with modifiers)
|
|
506
756
|
*/
|
|
507
|
-
function
|
|
508
|
-
|
|
509
|
-
|
|
757
|
+
function replaceWithStyleFunctionAttribute(
|
|
758
|
+
classNamePath: NodePath,
|
|
759
|
+
styleFunctionExpression: any,
|
|
760
|
+
targetStyleProp: string,
|
|
510
761
|
t: typeof BabelTypes,
|
|
511
762
|
) {
|
|
763
|
+
const styleAttribute = t.jsxAttribute(
|
|
764
|
+
t.jsxIdentifier(targetStyleProp),
|
|
765
|
+
t.jsxExpressionContainer(styleFunctionExpression),
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
classNamePath.replaceWith(styleAttribute);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Merge className style function with existing style prop (for Pressable with modifiers)
|
|
773
|
+
*/
|
|
774
|
+
function mergeStyleFunctionAttribute(
|
|
775
|
+
classNamePath: NodePath,
|
|
776
|
+
styleAttribute: any,
|
|
777
|
+
styleFunctionExpression: any,
|
|
778
|
+
t: typeof BabelTypes,
|
|
779
|
+
) {
|
|
780
|
+
const existingStyle = styleAttribute.value.expression;
|
|
781
|
+
|
|
782
|
+
// Create a wrapper function that merges both styles
|
|
783
|
+
// ({ pressed }) => [styleFunctionResult, existingStyle]
|
|
784
|
+
// We need to call the style function and merge results
|
|
785
|
+
|
|
786
|
+
// If existing is already a function, we need to handle it specially
|
|
787
|
+
if (t.isArrowFunctionExpression(existingStyle) || t.isFunctionExpression(existingStyle)) {
|
|
788
|
+
// Both are functions - create wrapper that calls both
|
|
789
|
+
// (_state) => [newStyleFn(_state), existingStyleFn(_state)]
|
|
790
|
+
// Create an identifier for the parameter to pass to the function calls
|
|
791
|
+
const paramIdentifier = t.identifier("_state");
|
|
792
|
+
|
|
793
|
+
const newFunctionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
|
|
794
|
+
const existingFunctionCall = t.callExpression(existingStyle, [paramIdentifier]);
|
|
795
|
+
|
|
796
|
+
const mergedArray = t.arrayExpression([newFunctionCall, existingFunctionCall]);
|
|
797
|
+
const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
|
|
798
|
+
|
|
799
|
+
styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
|
|
800
|
+
} else {
|
|
801
|
+
// Existing is static - create function that returns array
|
|
802
|
+
// (_state) => [styleFunctionResult, existingStyle]
|
|
803
|
+
// Create an identifier for the parameter to pass to the function call
|
|
804
|
+
const paramIdentifier = t.identifier("_state");
|
|
805
|
+
|
|
806
|
+
const functionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
|
|
807
|
+
const mergedArray = t.arrayExpression([functionCall, existingStyle]);
|
|
808
|
+
const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
|
|
809
|
+
|
|
810
|
+
styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Remove the className attribute
|
|
814
|
+
classNamePath.remove();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Inject StyleSheet.create with all collected styles
|
|
819
|
+
*/
|
|
820
|
+
function injectStyles(path: NodePath, styleRegistry: Map<string, StyleObject>, t: typeof BabelTypes) {
|
|
512
821
|
// Build style object properties
|
|
513
822
|
const styleProperties: any[] = [];
|
|
514
823
|
|
|
@@ -531,10 +840,10 @@ function injectStyles(
|
|
|
531
840
|
styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
|
|
532
841
|
}
|
|
533
842
|
|
|
534
|
-
// Create: const
|
|
843
|
+
// Create: const _tailwindStyles = StyleSheet.create({ ... })
|
|
535
844
|
const styleSheet = t.variableDeclaration("const", [
|
|
536
845
|
t.variableDeclarator(
|
|
537
|
-
t.identifier(
|
|
846
|
+
t.identifier(STYLES_IDENTIFIER),
|
|
538
847
|
t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
|
|
539
848
|
t.objectExpression(styleProperties),
|
|
540
849
|
]),
|
|
@@ -546,10 +855,7 @@ function injectStyles(
|
|
|
546
855
|
}
|
|
547
856
|
|
|
548
857
|
// Helper functions that use the imported parser
|
|
549
|
-
function parseClassName(
|
|
550
|
-
className: string,
|
|
551
|
-
customColors: Record<string, string>,
|
|
552
|
-
): Record<string, string | number> {
|
|
858
|
+
function parseClassName(className: string, customColors: Record<string, string>): StyleObject {
|
|
553
859
|
return parseClassNameFn(className, customColors);
|
|
554
860
|
}
|
|
555
861
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Pressable component with modifier support
|
|
3
|
+
* Injects disabled state into style function for disabled: modifier support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentRef } from "react";
|
|
7
|
+
import { forwardRef } from "react";
|
|
8
|
+
import {
|
|
9
|
+
Pressable as RNPressable,
|
|
10
|
+
type PressableStateCallbackType,
|
|
11
|
+
type PressableProps as RNPressableProps,
|
|
12
|
+
type StyleProp,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
|
|
16
|
+
// Extend PressableStateCallbackType to include disabled
|
|
17
|
+
type EnhancedPressableState = PressableStateCallbackType & { disabled: boolean | null | undefined };
|
|
18
|
+
|
|
19
|
+
export type PressableProps = Omit<RNPressableProps, "style"> & {
|
|
20
|
+
/**
|
|
21
|
+
* Style can be a static style object/array or a function that receives Pressable state + disabled
|
|
22
|
+
*/
|
|
23
|
+
style?: StyleProp<ViewStyle> | ((state: EnhancedPressableState) => StyleProp<ViewStyle>);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enhanced Pressable that supports the disabled: modifier
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* <Pressable
|
|
31
|
+
* disabled={isLoading}
|
|
32
|
+
* className="bg-blue-500 active:bg-blue-700 disabled:bg-gray-400"
|
|
33
|
+
* >
|
|
34
|
+
* <Text>Submit</Text>
|
|
35
|
+
* </Pressable>
|
|
36
|
+
*/
|
|
37
|
+
export const Pressable = forwardRef<ComponentRef<typeof RNPressable>, PressableProps>(function Pressable(
|
|
38
|
+
{ style, disabled = false, ...props },
|
|
39
|
+
ref,
|
|
40
|
+
) {
|
|
41
|
+
// Inject disabled into style function context
|
|
42
|
+
const resolvedStyle =
|
|
43
|
+
typeof style === "function" ? (state: PressableStateCallbackType) => style({ ...state, disabled }) : style;
|
|
44
|
+
|
|
45
|
+
return <RNPressable ref={ref} disabled={disabled} style={resolvedStyle} {...props} />;
|
|
46
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced TextInput component with focus state support for focus: modifier
|
|
3
|
+
*
|
|
4
|
+
* This component wraps React Native's TextInput and manages focus state internally,
|
|
5
|
+
* allowing the style prop to be a function that receives { focused: boolean }.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { TextInput } from '@mgcrea/react-native-tailwind';
|
|
10
|
+
*
|
|
11
|
+
* <TextInput
|
|
12
|
+
* className="border-2 border-gray-300 focus:border-blue-500 p-3 rounded-lg"
|
|
13
|
+
* placeholder="Email"
|
|
14
|
+
* />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { forwardRef, useCallback, useState } from "react";
|
|
19
|
+
import {
|
|
20
|
+
type BlurEvent,
|
|
21
|
+
type FocusEvent,
|
|
22
|
+
TextInput as RNTextInput,
|
|
23
|
+
type TextInputProps as RNTextInputProps,
|
|
24
|
+
} from "react-native";
|
|
25
|
+
|
|
26
|
+
export type TextInputProps = Omit<RNTextInputProps, "style"> & {
|
|
27
|
+
/**
|
|
28
|
+
* Style can be a static style object/array or a function that receives focus and disabled state
|
|
29
|
+
*/
|
|
30
|
+
style?:
|
|
31
|
+
| RNTextInputProps["style"]
|
|
32
|
+
| ((state: { focused: boolean; disabled: boolean }) => RNTextInputProps["style"]);
|
|
33
|
+
/**
|
|
34
|
+
* Convenience prop for disabled state (overrides editable if provided)
|
|
35
|
+
* When true, sets editable to false
|
|
36
|
+
*/
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Enhanced TextInput with focus and disabled state support
|
|
42
|
+
*
|
|
43
|
+
* Manages focus state internally and passes it to style functions,
|
|
44
|
+
* enabling the use of focus: and disabled: modifiers in className.
|
|
45
|
+
*
|
|
46
|
+
* Note: TextInput uses `editable` prop internally. You can pass either:
|
|
47
|
+
* - `disabled={true}` - convenience prop (sets editable to false)
|
|
48
|
+
* - `editable={false}` - React Native's native prop
|
|
49
|
+
* If both are provided, `disabled` takes precedence.
|
|
50
|
+
*/
|
|
51
|
+
export const TextInput = forwardRef<RNTextInput, TextInputProps>(function TextInput(
|
|
52
|
+
{ style, onFocus, onBlur, disabled, editable = true, ...props },
|
|
53
|
+
ref,
|
|
54
|
+
) {
|
|
55
|
+
const [focused, setFocused] = useState(false);
|
|
56
|
+
|
|
57
|
+
const handleFocus = useCallback(
|
|
58
|
+
(e: FocusEvent) => {
|
|
59
|
+
setFocused(true);
|
|
60
|
+
onFocus?.(e);
|
|
61
|
+
},
|
|
62
|
+
[onFocus],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const handleBlur = useCallback(
|
|
66
|
+
(e: BlurEvent) => {
|
|
67
|
+
setFocused(false);
|
|
68
|
+
onBlur?.(e);
|
|
69
|
+
},
|
|
70
|
+
[onBlur],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Resolve editable state: disabled prop overrides editable if provided
|
|
74
|
+
const isEditable = disabled !== undefined ? !disabled : editable;
|
|
75
|
+
const isDisabled = !isEditable;
|
|
76
|
+
|
|
77
|
+
// Resolve style - call function with focus and disabled state if needed
|
|
78
|
+
const resolvedStyle = typeof style === "function" ? style({ focused, disabled: isDisabled }) : style;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<RNTextInput
|
|
82
|
+
ref={ref}
|
|
83
|
+
style={resolvedStyle}
|
|
84
|
+
editable={isEditable}
|
|
85
|
+
onFocus={handleFocus}
|
|
86
|
+
onBlur={handleBlur}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
});
|