Skip to content

Commit 2ad5d19

Browse files
committed
Improve Babel Plugin support for TextInput component
Ensure `onFocus` attribute is added if not present already, so our wrapping logic can happen.
1 parent f0c75f1 commit 2ad5d19

File tree

4 files changed

+126
-9
lines changed

4 files changed

+126
-9
lines changed

packages/react-native-babel-plugin/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@
3939
},
4040
"dependencies": {
4141
"@babel/core": "^7.27.1",
42-
"@babel/helper-plugin-utils": "^7.27.1"
42+
"@babel/helper-plugin-utils": "^7.27.1",
43+
"@babel/types": "^7.27.1"
4344
},
4445
"devDependencies": {
4546
"@babel/cli": "^7.27.2",
4647
"@babel/preset-env": "^7.27.2",
4748
"@babel/preset-react": "^7.27.1",
4849
"@babel/preset-typescript": "^7.27.1",
49-
"@babel/types": "^7.27.1",
5050
"@swc/core": "^1.11.31",
5151
"@swc/jest": "^0.2.38",
5252
"@types/jest": "^29.5.14",

packages/react-native-babel-plugin/src/actions/rum/index.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@
55
*/
66

77
import type * as Babel from '@babel/core';
8+
import {
9+
arrowFunctionExpression,
10+
blockStatement,
11+
jsxAttribute,
12+
jsxExpressionContainer,
13+
jsxIdentifier
14+
} from '@babel/types';
815

9-
import { RumActionConstants, rumComponentAttributes } from '../../constants';
16+
import {
17+
RumActionConstants,
18+
rumComponentAttributes,
19+
tapElementsRequiredAttributesMap
20+
} from '../../constants';
1021
import type {
1122
PluginPassState,
1223
PluginOptions,
@@ -39,23 +50,62 @@ export function handleJSXElementActionPaths(
3950
state: PluginPassState,
4051
options: PluginOptions
4152
) {
42-
const { actionPathList, ddValues } = getJSXElementActionPaths(
43-
componentName,
44-
t,
53+
const {
54+
actionPathList,
55+
actionPathNames,
56+
ddValues
57+
} = getJSXElementActionPaths(componentName, t, path, state, options);
58+
59+
ensureMandatoryAttributes(
4560
path,
46-
state,
47-
options
61+
componentName,
62+
actionPathList,
63+
actionPathNames
4864
);
4965

5066
for (const attrPath of actionPathList) {
5167
attrPath.node.extra = {
5268
...attrPath.node.extra,
5369
ddValues
5470
};
71+
5572
handleRumActions(t, attrPath, state);
5673
}
5774
}
5875

76+
export function ensureMandatoryAttributes(
77+
path: Babel.NodePath<Babel.types.JSXElement>,
78+
componentName: string,
79+
actionPathList: Babel.NodePath<Babel.types.JSXAttribute>[],
80+
actionPathNames: string[]
81+
) {
82+
// Check if we're missing some required attributes
83+
const requiredAttributes = tapElementsRequiredAttributesMap[componentName];
84+
if (requiredAttributes) {
85+
const attrToAdd = requiredAttributes.filter(
86+
x => !actionPathNames.includes(x)
87+
);
88+
89+
for (const attr of attrToAdd) {
90+
const attribute = jsxAttribute(
91+
jsxIdentifier(attr),
92+
jsxExpressionContainer(
93+
arrowFunctionExpression([], blockStatement([]))
94+
)
95+
);
96+
path.node.openingElement.attributes.push(attribute);
97+
98+
const attrPaths = path.get(
99+
'openingElement.attributes'
100+
) as Babel.NodePath<Babel.types.JSXAttribute>[];
101+
102+
const lastPath = attrPaths[attrPaths.length - 1];
103+
104+
actionPathList.push(lastPath);
105+
}
106+
}
107+
}
108+
59109
export function getJSXElementActionPaths(
60110
componentName: string,
61111
t: typeof Babel.types,
@@ -71,6 +121,7 @@ export function getJSXElementActionPaths(
71121
const ddValues: Record<string, string> = {};
72122
const actionMapList = state.tapMappings?.[componentName] || [];
73123
const actionPathList: Babel.NodePath<Babel.types.JSXAttribute>[] = [];
124+
const actionPathNames: string[] = [];
74125

75126
path.traverse({
76127
JSXAttribute(subpath) {
@@ -98,13 +149,14 @@ export function getJSXElementActionPaths(
98149
const isValidMapping = actionMapList.includes(attrName);
99150

100151
if (isValidMapping) {
152+
actionPathNames.push(attrName);
101153
actionPathList.push(subpath);
102154
return;
103155
}
104156
}
105157
});
106158

107-
return { actionPathList, ddValues };
159+
return { actionPathList, actionPathNames, ddValues };
108160
}
109161

110162
export function handleRumActions(

packages/react-native-babel-plugin/src/constants/rum.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export const tapElementsMap: Record<string, string[]> = {
3535
TextInput: ['onFocus']
3636
};
3737

38+
export const tapElementsRequiredAttributesMap: Record<string, string[]> = {
39+
TextInput: ['onFocus']
40+
};
41+
3842
export const rumComponentAttributes = [
3943
'dd-action-name',
4044
'accessibilityLabel'

packages/react-native-babel-plugin/test/plugin.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,67 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
6262
`);
6363
});
6464

65+
it('should wrap supported property (onFocus) on supported element (TextInput)', () => {
66+
const input = `
67+
import { TextInput } from 'react-native';
68+
<TextInput
69+
placeholder="Enter username"
70+
value={username}
71+
onChangeText={setUsername}
72+
style={styles.input}
73+
onFocus={() => { console.log('test'); }}
74+
/>
75+
`;
76+
const output = transformCode(input);
77+
expect(output).toMatchInlineSnapshot(`
78+
"import { DdBabelInteractionTracking } from "@datadog/mobile-react-native";
79+
import { TextInput } from 'react-native';
80+
/*#__PURE__*/React.createElement(TextInput, {
81+
placeholder: "Enter username",
82+
value: username,
83+
onChangeText: setUsername,
84+
style: styles.input,
85+
onFocus: () => {
86+
if (DdBabelInteractionTracking.getInstance()) return DdBabelInteractionTracking.getInstance().wrapRumAction(() => {
87+
console.log('test');
88+
}, "TAP", {
89+
"componentName": "TextInput"
90+
})();else return (() => {
91+
console.log('test');
92+
})();
93+
}
94+
});"
95+
`);
96+
});
97+
98+
it('should add mandatory property (onFocus) on supported element (TextInput) when not present', () => {
99+
const input = `
100+
import { TextInput } from 'react-native';
101+
<TextInput
102+
placeholder="Enter username"
103+
value={username}
104+
onChangeText={setUsername}
105+
style={styles.input}
106+
/>
107+
`;
108+
const output = transformCode(input);
109+
expect(output).toMatchInlineSnapshot(`
110+
"import { DdBabelInteractionTracking } from "@datadog/mobile-react-native";
111+
import { TextInput } from 'react-native';
112+
/*#__PURE__*/React.createElement(TextInput, {
113+
placeholder: "Enter username",
114+
value: username,
115+
onChangeText: setUsername,
116+
style: styles.input,
117+
onFocus: () => {
118+
if (DdBabelInteractionTracking.getInstance()) return DdBabelInteractionTracking.getInstance().wrapRumAction(() => {}, "TAP", {
119+
"componentName": "TextInput"
120+
})();else return (() => {})();
121+
}
122+
});"
123+
`);
124+
});
125+
65126
it('should wrap arrow function with one argument', () => {
66127
const input = `
67128
import { Pressable } from 'react-native';

0 commit comments

Comments
 (0)