Skip to content

Commit efb95c7

Browse files
authored
Support transformOrigin and percentage based values on the translate utility (#356)
1 parent d32ffbf commit efb95c7

File tree

13 files changed

+3988
-2376
lines changed

13 files changed

+3988
-2376
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: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
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: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { TwConfig } from './tw-config';
2-
import type { StyleIR, DeviceContext, ParseContext, Platform } from './types';
2+
import type { StyleIR, DeviceContext, ParseContext, Platform, Version } from './types';
33
import type Cache from './cache';
44
import fontSize from './resolve/font-size';
55
import lineHeight from './resolve/line-height';
@@ -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
import pointerEvents from './resolve/pointer-events';
2128

2229
export default class UtilityParser {
@@ -34,8 +41,10 @@ export default class UtilityParser {
3441
private cache: Cache,
3542
device: DeviceContext,
3643
platform: Platform,
44+
reactNativeVersion: Version,
3745
) {
3846
this.context.device = device;
47+
this.context.reactNativeVersion = reactNativeVersion;
3948
const parts = input.trim().split(`:`);
4049
let prefixes: string[] = [];
4150
if (parts.length === 1) {
@@ -326,6 +335,11 @@ export default class UtilityParser {
326335
return transformNone();
327336
}
328337

338+
if (this.consumePeeked(`origin-`)) {
339+
style = origin(this.rest, this.context, theme?.transformOrigin);
340+
if (style) return style;
341+
}
342+
329343
if (this.consumePeeked(`pointer-events-`)) {
330344
style = pointerEvents(this.rest);
331345
if (style) return style;

src/__tests__/color-scheme.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import type { TailwindFn } from '../';
66
import { create, useDeviceContext, useAppColorScheme } from '../';
77

88
jest.mock(`react-native`, () => ({
9-
Platform: { OS: `ios` },
9+
Platform: {
10+
OS: `ios`,
11+
constants: { reactNativeVersion: { major: 0, minor: 75, patch: 0 } },
12+
},
1013
useColorScheme: () => `light`,
1114
useWindowDimensions: () => ({ width: 320, height: 640, fontScale: 1, scale: 2 }),
1215
}));

src/__tests__/prefix-match.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { describe, test, expect } from '@jest/globals';
33
import { create } from '../';
44

55
jest.mock(`react-native`, () => ({
6-
Platform: { OS: `ios` },
6+
Platform: {
7+
OS: `ios`,
8+
constants: { reactNativeVersion: { major: 0, minor: 75, patch: 0 } },
9+
},
710
}));
811

912
describe(`tw.prefixMatch()`, () => {

src/__tests__/transform.spec.ts

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import rn from 'react-native';
12
import { create } from '../';
23

34
describe(`transform utilities`, () => {
@@ -207,12 +208,6 @@ describe(`transform utilities`, () => {
207208
// not configged
208209
[`translate-x-81`, { transform: [{ translateX: (81 / 4) * 16 }] }],
209210
[`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`, {}],
216211
];
217212

218213
test.each(cases)(`tw\`%s\` -> %s`, (utility, expected) => {
@@ -246,6 +241,54 @@ describe(`transform utilities`, () => {
246241
transform: [{ translateY: 16 }],
247242
});
248243
});
244+
245+
test(`translate w/percentage values`, () => {
246+
rn.Platform.constants.reactNativeVersion = {
247+
major: 0,
248+
minor: 75,
249+
patch: 0,
250+
};
251+
252+
expect(tw.style(`translate-x-full`)).toMatchObject({
253+
transform: [{ translateX: `100%` }],
254+
});
255+
expect(tw.style(`translate-y-full`)).toMatchObject({
256+
transform: [{ translateY: `100%` }],
257+
});
258+
expect(tw.style(`translate-x-1/2`)).toMatchObject({
259+
transform: [{ translateX: `50%` }],
260+
});
261+
expect(tw.style(`translate-y-1/2`)).toMatchObject({
262+
transform: [{ translateY: `50%` }],
263+
});
264+
expect(tw.style(`translate-x-[10%]`)).toMatchObject({
265+
transform: [{ translateX: `10%` }],
266+
});
267+
expect(tw.style(`translate-y-[10%]`)).toMatchObject({
268+
transform: [{ translateY: `10%` }],
269+
});
270+
expect(tw.style(`translate-x-1/5`)).toMatchObject({
271+
transform: [{ translateX: `20%` }],
272+
});
273+
expect(tw.style(`translate-y-1/5`)).toMatchObject({
274+
transform: [{ translateY: `20%` }],
275+
});
276+
277+
rn.Platform.constants.reactNativeVersion = {
278+
major: 0,
279+
minor: 74,
280+
patch: 0,
281+
};
282+
283+
expect(tw.style(`translate-x-full`)).toMatchObject({});
284+
expect(tw.style(`translate-y-full`)).toMatchObject({});
285+
expect(tw.style(`translate-x-1/2`)).toMatchObject({});
286+
expect(tw.style(`translate-y-1/2`)).toMatchObject({});
287+
expect(tw.style(`translate-x-[10%]`)).toMatchObject({});
288+
expect(tw.style(`translate-y-[10%]`)).toMatchObject({});
289+
expect(tw.style(`translate-x-1/5`)).toMatchObject({});
290+
expect(tw.style(`translate-y-1/5`)).toMatchObject({});
291+
});
249292
});
250293

251294
test(`combine multiple transform utilities `, () => {
@@ -276,4 +319,59 @@ describe(`transform utilities`, () => {
276319
),
277320
).toMatchObject({ transform: [] });
278321
});
322+
323+
describe(`origin`, () => {
324+
const cases: Array<
325+
[string, Record<'transformOrigin', string> | Record<string, never>]
326+
> = [
327+
[`origin-center`, { transformOrigin: `center` }],
328+
[`origin-top`, { transformOrigin: `top` }],
329+
[`origin-top-right`, { transformOrigin: `top right` }],
330+
[`origin-right`, { transformOrigin: `right` }],
331+
[`origin-bottom-right`, { transformOrigin: `bottom right` }],
332+
[`origin-bottom`, { transformOrigin: `bottom` }],
333+
[`origin-bottom-left`, { transformOrigin: `bottom left` }],
334+
[`origin-left`, { transformOrigin: `left` }],
335+
[`origin-top-left`, { transformOrigin: `top left` }],
336+
337+
// arbitrary
338+
[`origin-[top]`, { transformOrigin: `top` }],
339+
[`origin-[10%]`, { transformOrigin: `10%` }],
340+
[`origin-[10px]`, { transformOrigin: `10px` }],
341+
[`origin-[left_top]`, { transformOrigin: `left top` }],
342+
[`origin-[bottom_right]`, { transformOrigin: `bottom right` }],
343+
[`origin-[center_center]`, { transformOrigin: `center center` }],
344+
[`origin-[center_10%]`, { transformOrigin: `center 10%` }],
345+
[`origin-[10px_center]`, { transformOrigin: `10px center` }],
346+
[`origin-[10px_10%]`, { transformOrigin: `10px 10%` }],
347+
[`origin-[-10%_20%_10px]`, { transformOrigin: `-10% 20% 10px` }],
348+
[`origin-[-10px_-10px_-10px]`, { transformOrigin: `-10px -10px -10px` }],
349+
[`origin-[left_top_10px]`, { transformOrigin: `left top 10px` }],
350+
351+
// invalid
352+
[`origin-[left_left]`, {}],
353+
[`origin-[top_top]`, {}],
354+
[`origin-[top_left_10%]`, {}],
355+
];
356+
357+
test.each(cases)(`tw\`%s\` -> %s`, (utility, expected) => {
358+
expect(tw.style(utility)).toMatchObject(expected);
359+
});
360+
361+
test(`origin w/extended theme`, () => {
362+
tw = create({
363+
theme: {
364+
extend: {
365+
transformOrigin: {
366+
custom: `33% 75% 10px`,
367+
},
368+
},
369+
},
370+
});
371+
372+
expect(tw.style(`origin-custom`)).toMatchObject({
373+
transformOrigin: `33% 75% 10px`,
374+
});
375+
});
376+
});
279377
});

src/__tests__/tw.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import type { TwConfig } from '../tw-config';
55
import { create } from '../';
66

77
jest.mock(`react-native`, () => ({
8-
Platform: { OS: `ios` },
8+
Platform: {
9+
OS: `ios`,
10+
constants: { reactNativeVersion: { major: 0, minor: 75, patch: 0 } },
11+
},
912
}));
1013

1114
describe(`tw`, () => {

src/create.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
StyleIR,
1010
DeviceContext,
1111
Platform,
12+
Version,
1213
} from './types';
1314
import type { TwConfig } from './tw-config';
1415
import Cache from './cache';
@@ -18,7 +19,11 @@ import { parseInputs } from './parse-inputs';
1819
import { complete, warn } from './helpers';
1920
import { getAddedUtilities } from './plugin';
2021

21-
export function create(customConfig: TwConfig, platform: Platform): TailwindFn {
22+
export function create(
23+
customConfig: TwConfig,
24+
platform: Platform,
25+
reactNativeVersion: Version,
26+
): TailwindFn {
2227
const config = resolveConfig(withContent(customConfig) as any) as TwConfig;
2328
const device: DeviceContext = {};
2429

@@ -100,7 +105,14 @@ export function create(customConfig: TwConfig, platform: Platform): TailwindFn {
100105
for (const utility of utilities) {
101106
let styleIr = cache.getIr(utility);
102107
if (!styleIr) {
103-
const parser = new UtilityParser(utility, config, cache, device, platform);
108+
const parser = new UtilityParser(
109+
utility,
110+
config,
111+
cache,
112+
device,
113+
platform,
114+
reactNativeVersion,
115+
);
104116
styleIr = parser.parse();
105117
}
106118

@@ -184,7 +196,14 @@ export function create(customConfig: TwConfig, platform: Platform): TailwindFn {
184196
if (cached !== undefined) {
185197
return cached;
186198
}
187-
const parser = new UtilityParser(`${joined}:flex`, config, cache, device, platform);
199+
const parser = new UtilityParser(
200+
`${joined}:flex`,
201+
config,
202+
cache,
203+
device,
204+
platform,
205+
reactNativeVersion,
206+
);
188207
const ir = parser.parse();
189208
const prefixMatches = ir.kind !== `null`;
190209
cache.setPrefixMatch(joined, prefixMatches);

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import type { TwConfig } from './tw-config';
44
import plugin from './plugin';
55
import rawCreate from './create';
66

7-
// Apply default config and inject RN Platform
8-
const create = (twConfig: TwConfig = {}): TailwindFn => rawCreate(twConfig, Platform.OS);
7+
// Apply default config and inject RN Platform and RN version
8+
const create = (twConfig: TwConfig = {}): TailwindFn =>
9+
rawCreate(twConfig, Platform.OS, Platform.constants.reactNativeVersion);
910

1011
export type { TailwindFn, TwConfig, RnColorScheme, ClassInput, Style };
1112
export { useDeviceContext, useAppColorScheme } from './hooks';

0 commit comments

Comments
 (0)