Skip to content

Commit b4ce46c

Browse files
committed
added support for percentage values in translate and for transformOrigin
1 parent 5930151 commit b4ce46c

File tree

7 files changed

+3884
-2371
lines changed

7 files changed

+3884
-2371
lines changed

package-lock.json

Lines changed: 3713 additions & 2351 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,12 @@
4646
"tailwindcss": ">=2.0.0 <4.0.0"
4747
},
4848
"peerDependencies": {
49-
"react-native": ">=0.62.2"
49+
"react-native": ">=0.75.0"
5050
},
5151
"devDependencies": {
5252
"@babel/preset-typescript": "^7.23.3",
5353
"@types/jest": "^29.5.11",
5454
"@types/react": "^18.2.55",
55-
"@types/react-native": "^0.73.0",
5655
"@types/react-test-renderer": "^18.0.7",
5756
"@types/tailwindcss": "^3.1.0",
5857
"@typescript-eslint/eslint-plugin": "^6.15.0",
@@ -65,7 +64,7 @@
6564
"metro-react-native-babel-preset": "^0.66.2",
6665
"prettier": "^3.1.1",
6766
"react": "^18.2.0",
68-
"react-native": "^0.73.4",
67+
"react-native": "^0.75.0",
6968
"react-test-renderer": "^18.2.0",
7069
"ts-jest": "^29.1.1",
7170
"typescript": "^5.3.3"

src/UtilityParser.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ import { widthHeight, size, minMaxWidthHeight } from './resolve/width-height';
1616
import { letterSpacing } from './resolve/letter-spacing';
1717
import { opacity } from './resolve/opacity';
1818
import { shadowOpacity, shadowOffset } from './resolve/shadow';
19-
import { rotate, scale, skew, transformNone, translate } from './resolve/transform';
19+
import {
20+
origin,
21+
rotate,
22+
scale,
23+
skew,
24+
transformNone,
25+
translate,
26+
} from './resolve/transform';
2027

2128
export default class UtilityParser {
2229
private position = 0;
@@ -325,6 +332,11 @@ export default class UtilityParser {
325332
return transformNone();
326333
}
327334

335+
if (this.consumePeeked(`origin-`)) {
336+
style = origin(this.rest, this.context, theme?.transformOrigin);
337+
if (style) return style;
338+
}
339+
328340
h.warn(`\`${this.rest}\` unknown or invalid utility`);
329341
return null;
330342
}

src/__tests__/transform.spec.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,20 +199,22 @@ describe(`transform utilities`, () => {
199199
[`-translate-y-px`, { transform: [{ translateY: -1 }] }],
200200
[`-translate-x-0.5`, { transform: [{ translateX: -2 }] }],
201201
[`-translate-y-0.5`, { transform: [{ translateY: -2 }] }],
202+
[`translate-x-full`, { transform: [{ translateX: `100%` }] }],
203+
[`translate-y-full`, { transform: [{ translateY: `100%` }] }],
204+
[`translate-x-1/2`, { transform: [{ translateX: `50%` }] }],
205+
[`translate-y-1/2`, { transform: [{ translateY: `50%` }] }],
202206

203207
// arbitrary
204208
[`translate-x-[17rem]`, { transform: [{ translateX: 272 }] }],
205209
[`translate-y-[17rem]`, { transform: [{ translateY: 272 }] }],
210+
[`translate-x-[10%]`, { transform: [{ translateX: `10%` }] }],
211+
[`translate-y-[10%]`, { transform: [{ translateY: `10%` }] }],
206212

207213
// not configged
208214
[`translate-x-81`, { transform: [{ translateX: (81 / 4) * 16 }] }],
209215
[`translate-y-81`, { transform: [{ translateY: (81 / 4) * 16 }] }],
210-
211-
// unsupported
212-
[`translate-x-full`, {}],
213-
[`translate-y-full`, {}],
214-
[`translate-x-1/2`, {}],
215-
[`translate-y-1/2`, {}],
216+
[`translate-x-1/5`, { transform: [{ translateX: `20%` }] }],
217+
[`translate-y-1/5`, { transform: [{ translateY: `20%` }] }],
216218
];
217219

218220
test.each(cases)(`tw\`%s\` -> %s`, (utility, expected) => {
@@ -276,4 +278,48 @@ describe(`transform utilities`, () => {
276278
),
277279
).toMatchObject({ transform: [] });
278280
});
281+
282+
describe(`origin`, () => {
283+
const cases: Array<
284+
[string, Record<'transformOrigin', string | (string | number)[]>]
285+
> = [
286+
[`origin-center`, { transformOrigin: `center` }],
287+
[`origin-top`, { transformOrigin: `top` }],
288+
[`origin-top-right`, { transformOrigin: `top right` }],
289+
[`origin-right`, { transformOrigin: `right` }],
290+
[`origin-bottom-right`, { transformOrigin: `bottom right` }],
291+
[`origin-bottom`, { transformOrigin: `bottom` }],
292+
[`origin-bottom-left`, { transformOrigin: `bottom left` }],
293+
[`origin-left`, { transformOrigin: `left` }],
294+
[`origin-top-left`, { transformOrigin: `top left` }],
295+
296+
// arbitrary
297+
[`origin-[top]`, { transformOrigin: [`top`] }],
298+
[`origin-[10%]`, { transformOrigin: [`10%`] }],
299+
[`origin-[10px]`, { transformOrigin: [10] }],
300+
[`origin-[left_top]`, { transformOrigin: [`left`, `top`] }],
301+
[`origin-[-10%_20%_10px]`, { transformOrigin: [`-10%`, `20%`, 10] }],
302+
[`origin-[-10px_-10px_-10px]`, { transformOrigin: [-10, -10, -10] }],
303+
];
304+
305+
test.each(cases)(`tw\`%s\` -> %s`, (utility, expected) => {
306+
expect(tw.style(utility)).toMatchObject(expected);
307+
});
308+
309+
test(`origin w/extended theme`, () => {
310+
tw = create({
311+
theme: {
312+
extend: {
313+
transformOrigin: {
314+
'top-left-1/3-3/4-10': `33% 75% 10px`,
315+
},
316+
},
317+
},
318+
});
319+
320+
expect(tw.style(`origin-top-left-1/3-3/4-10`)).toMatchObject({
321+
transformOrigin: `33% 75% 10px`,
322+
});
323+
});
324+
});
279325
});

src/resolve/transform.ts

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { TwTheme } from '../tw-config';
22
import type { DependentStyle, ParseContext, Style, StyleIR } from '../types';
3-
import { isString, Unit } from '../types';
3+
import { Unit } from '../types';
44
import {
5+
complete,
56
parseNumericValue,
67
parseStyleVal,
78
parseUnconfigged,
@@ -10,6 +11,7 @@ import {
1011

1112
type Axis = `x` | `y` | `z` | ``;
1213
type Property = `scale` | `rotate` | `skew` | `translate`;
14+
type Position = `left` | `center` | `right` | `top` | `bottom`;
1315

1416
export function scale(
1517
value: string,
@@ -124,13 +126,7 @@ export function translate(
124126
? parseStyleVal(configValue, context)
125127
: parseUnconfigged(value, context);
126128

127-
// support for percentage values in translate was only added in RN 0.75
128-
// source: https://reactnative.dev/blog/2024/08/12/release-0.75#percentage-values-in-translation
129-
// since the support of this package starts at RN 0.62.2
130-
// we need to filter out percentages which are non-numeric values
131-
return styleVal === null || isString(styleVal)
132-
? null
133-
: createStyle(styleVal, `translate`, translateAxis);
129+
return styleVal === null ? null : createStyle(styleVal, `translate`, translateAxis);
134130
}
135131

136132
export function transformNone(): StyleIR {
@@ -142,6 +138,75 @@ export function transformNone(): StyleIR {
142138
};
143139
}
144140

141+
export function origin(
142+
value: string,
143+
context: ParseContext = {},
144+
config?: TwTheme['transformOrigin'],
145+
): StyleIR | null {
146+
const configValue = config?.[value];
147+
148+
if (configValue) {
149+
return complete({ transformOrigin: configValue });
150+
}
151+
152+
if (isArbitraryValue(value)) {
153+
const values = value.slice(1, -1).split(`_`);
154+
155+
if (values.length === 0 || values.length > 3) {
156+
return null;
157+
}
158+
159+
// with a single value, the value must be one of the positions, a percentage or a pixel value
160+
if (values.length === 1) {
161+
const parsedValue = parseOriginValue(
162+
values[0],
163+
[`left`, `center`, `right`, `top`, `bottom`],
164+
[Unit.percent, Unit.px],
165+
context,
166+
);
167+
return parsedValue === null ? null : complete({ transformOrigin: [parsedValue] });
168+
}
169+
170+
// with two or three values, the first value must be 'left' / 'center' / 'right', a percentage or a pixel value
171+
const xOffset = parseOriginValue(
172+
values[0],
173+
[`left`, `center`, `right`],
174+
[Unit.percent, Unit.px],
175+
context,
176+
);
177+
178+
if (xOffset === null) {
179+
return null;
180+
}
181+
182+
// with two or three values, the second value must be 'top' / 'center' / 'bottom', a percentage or a pixel value
183+
const yOffset = parseOriginValue(
184+
values[1],
185+
[`top`, `center`, `bottom`],
186+
[Unit.percent, Unit.px],
187+
context,
188+
);
189+
190+
if (yOffset === null) {
191+
return null;
192+
}
193+
194+
// with three values, the third value must be a pixel value
195+
const zOffset = parseOriginValue(values[2], [], [Unit.px], context);
196+
197+
if (zOffset === null && values.length === 3) {
198+
return null;
199+
}
200+
201+
return complete({
202+
transformOrigin:
203+
zOffset === null ? [xOffset, yOffset] : [xOffset, yOffset, zOffset],
204+
});
205+
}
206+
207+
return null;
208+
}
209+
145210
function createStyle(
146211
styleVal: string | number,
147212
property: Property,
@@ -166,6 +231,27 @@ function createStyle(
166231
};
167232
}
168233

169-
function isArbitraryValue(value: string): boolean {
234+
export function isArbitraryValue(value: string): boolean {
170235
return value.startsWith(`[`) && value.endsWith(`]`);
171236
}
237+
238+
function parseOriginValue(
239+
value: string | undefined,
240+
allowedPositions: Position[],
241+
allowedUnits: Unit[],
242+
context: ParseContext = {},
243+
): string | number | null {
244+
if (!value) {
245+
return null;
246+
}
247+
248+
if (allowedPositions.includes(value as any)) {
249+
return value;
250+
}
251+
252+
const parsedValue = parseNumericValue(value, context);
253+
254+
return parsedValue === null || !allowedUnits.includes(parsedValue[1])
255+
? null
256+
: toStyleVal(parsedValue[0], parsedValue[1], context);
257+
}

src/tw-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface TwTheme {
4646
rotate?: Record<string, string>;
4747
skew?: Record<string, string>;
4848
translate?: Record<string, string>;
49+
transformOrigin?: Record<string, string>;
4950
extend?: Omit<TwTheme, 'extend'>;
5051
}
5152

src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ export type Direction =
108108
| 'BottomRight';
109109

110110
export type Style = {
111-
[key: string]: string[] | string | number | boolean | Style | Style[];
111+
[key: string]:
112+
| string[]
113+
| (string | number)[]
114+
| string
115+
| number
116+
| boolean
117+
| Style
118+
| Style[];
112119
};
113120

114121
export enum ConfigType {

0 commit comments

Comments
 (0)