Skip to content

Commit f64c614

Browse files
feat: add web support via CSS transitions (#3)
* feat: add web support via CSS transitions Implement EaseView.web.tsx that uses CSS transitions for timing animations, CSS @Keyframes for loop animations, and a timing approximation for springs. React Native's .web.tsx resolution automatically picks this up on web. * feat(example): add Expo for web support Install expo and web dependencies (react-dom, react-native-web, @expo/metro-runtime) to enable running the example app on web via `npx expo start --web`. * chore(example): expo web setup - fix web entry config * fix(web): use StyleSheet.flatten to resolve RN style IDs/arrays The web EaseView receives RN StyleSheet numeric IDs and style arrays, but was trying to spread them directly as React.CSSProperties. Spreading an array gives {0: id} which caused: TypeError: Failed to set an indexed property [0] on CSSStyleDeclaration Fix: use StyleSheet.flatten() to resolve any RN StyleProp to a plain object before spreading into computedStyle. * refactor(web): use View from react-native instead of raw div Replace the raw <div> with react-native View which: - Handles StyleSheet flattening automatically (no more manual flattenStyle) - Accepts StyleProp<ViewStyle> natively - Converts RN transform arrays to CSS transforms via react-native-web - Properly handles all RN style types (numeric IDs, arrays, objects) CSS transitions and keyframe animations are applied imperatively via the DOM ref, since View does not expose those as style props. Also removes accidentally committed .playwright-cli artifacts. * fix(web): don't override style border-radius/backgroundColor in loop keyframes Only include border-radius and background-color in CSS @Keyframes when the user explicitly sets them in animate/initialAnimate props. Previously, IDENTITY defaults (borderRadius: 0) were always included, which overrode values set via the style prop (e.g. pulse's borderRadius: 30). * fix(web): remove will-change hint to fix overflow:hidden clipping will-change: transform causes the element to be promoted to its own GPU compositing layer, which can escape parent overflow:hidden clipping in some browsers. CSS animations already get GPU-promoted automatically, so the hint is unnecessary. * fix(example): use useWindowDimensions for banner width on web BANNER_WIDTH was computed at module load time using Dimensions.get('window') which bakes in the build-time window width on static web exports. Switched to useWindowDimensions() hook so the banner correctly sizes to the actual viewport width at render time. * fix(web): add dom lib to tsconfig, fix viewRef type, fix app registration for native * fix(example): remove duplicate gradle-plugin includeBuild for Android CI * fix(example): fix Android build - deduplicate gradle-plugin includeBuild * fix(example): fix Android CI - use expo autolinking for gradle-plugin includeBuild * fix(example): restore gradle-plugin includeBuild in pluginManagement for settings plugin
1 parent 899d15d commit f64c614

22 files changed

Lines changed: 2755 additions & 112 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,5 @@ android/generated
8484

8585
# React Native Nitro Modules
8686
nitrogen/
87+
.playwright-cli/
88+
*.png

example/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
# Expo
3+
.expo
4+
dist/
5+
web-build/

example/android/app/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ react {
5252

5353
/* Autolinking */
5454
autolinkLibrariesWithApp()
55+
//
56+
// Added by install-expo-modules
57+
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", rootDir.getAbsoluteFile().getParentFile().getAbsolutePath(), "android", "absolute"].execute(null, rootDir).text.trim())
58+
cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim())
59+
bundleCommand = "export:embed"
5560
}
5661

5762
/**

example/android/app/src/main/java/ease/example/MainActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package ease.example
2+
import expo.modules.ReactActivityDelegateWrapper
23

34
import com.facebook.react.ReactActivity
45
import com.facebook.react.ReactActivityDelegate
@@ -18,5 +19,5 @@ class MainActivity : ReactActivity() {
1819
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
1920
*/
2021
override fun createReactActivityDelegate(): ReactActivityDelegate =
21-
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
22+
ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled))
2223
}

example/android/app/src/main/java/ease/example/MainApplication.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
package ease.example
2+
import android.content.res.Configuration
3+
import expo.modules.ApplicationLifecycleDispatcher
4+
import expo.modules.ExpoReactHostFactory
25

36
import android.app.Application
47
import com.facebook.react.PackageList
@@ -10,7 +13,7 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
1013
class MainApplication : Application(), ReactApplication {
1114

1215
override val reactHost: ReactHost by lazy {
13-
getDefaultReactHost(
16+
ExpoReactHostFactory.getDefaultReactHost(
1417
context = applicationContext,
1518
packageList =
1619
PackageList(this).packages.apply {
@@ -23,5 +26,11 @@ class MainApplication : Application(), ReactApplication {
2326
override fun onCreate() {
2427
super.onCreate()
2528
loadReactNative(this)
29+
ApplicationLifecycleDispatcher.onApplicationCreate(this)
30+
}
31+
32+
override fun onConfigurationChanged(newConfig: Configuration) {
33+
super.onConfigurationChanged(newConfig)
34+
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
2635
}
2736
}

example/android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ buildscript {
1919
}
2020

2121
apply plugin: "com.facebook.react.rootproject"
22+
apply plugin: "expo-root-project"

example/android/settings.gradle

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1-
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
2-
plugins { id("com.facebook.react.settings") }
3-
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
1+
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin")
2+
def expoPluginsPath = new File(
3+
providers.exec {
4+
workingDir(rootDir)
5+
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
6+
}.standardOutput.asText.get().trim(),
7+
"../android/expo-gradle-plugin"
8+
).absolutePath
9+
includeBuild(expoPluginsPath)
10+
}
11+
plugins { id("com.facebook.react.settings")
12+
id("expo-autolinking-settings")
13+
}
14+
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) }
415
rootProject.name = 'ease.example'
516
include ':app'
617
includeBuild('../node_modules/@react-native/gradle-plugin')
18+
expoAutolinking.useExpoModules()
19+
expoAutolinking.useExpoVersionCatalog()

example/app.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
{
22
"name": "EaseExample",
3-
"displayName": "EaseExample"
4-
}
3+
"displayName": "EaseExample",
4+
"expo": {
5+
"name": "EaseExample",
6+
"slug": "ease-example",
7+
"version": "1.0.0",
8+
"web": {
9+
"bundler": "metro",
10+
"output": "single",
11+
"entry": "./index.js"
12+
},
13+
"platforms": [
14+
"ios",
15+
"android",
16+
"web"
17+
]
18+
}
19+
}

example/babel.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const root = path.resolve(__dirname, '..');
66

77
module.exports = getConfig(
88
{
9-
presets: ['module:@react-native/babel-preset'],
9+
presets: ['babel-preset-expo'],
1010
plugins: ['react-native-worklets/plugin'],
1111
},
1212
{ root, pkg },

example/index.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { AppRegistry } from 'react-native';
1+
import { AppRegistry, Platform } from 'react-native';
22
import App from './src/App';
3-
import { name as appName } from './app.json';
43

5-
AppRegistry.registerComponent(appName, () => App);
4+
AppRegistry.registerComponent('EaseExample', () => App);
5+
6+
if (Platform.OS === 'web') {
7+
const rootTag = document.getElementById('root');
8+
AppRegistry.runApplication('EaseExample', { rootTag });
9+
}

0 commit comments

Comments
 (0)