react-native-device-posture 0.1.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 ADDED
@@ -0,0 +1,59 @@
1
+ # react-native-device-posture
2
+
3
+ One fold and hinge status for iOS and Android. Wrap the app once, then read the same state from any screen.
4
+
5
+ Both platforms emit the listener `onFoldStateChange`.
6
+
7
+ | `status` | Meaning | `isFlat` |
8
+ |---|---|---|
9
+ | `unknown` | This device has no hinge | `false` |
10
+ | `closed` | Folded | `false` |
11
+ | `partiallyOpen` | Mid-fold | `false` |
12
+ | `fullyOpen` | Unfolded flat | `true` |
13
+
14
+ `isFlat` is true only when the device is fully open. Use that for the wide Duo layout.
15
+
16
+ iOS reads `UIHinge` (iOS 27.1 and later). Android reads Jetpack WindowManager `FoldingFeature`. Phones without a hinge stay on `unknown`.
17
+
18
+ Requires React Native 0.76 or newer with the New Architecture enabled.
19
+
20
+ ## Install
21
+
22
+ From the app:
23
+
24
+ ```sh
25
+ npm install /Users/shubham/Documents/Learning/react-native-device-posture
26
+ cd ios && pod install
27
+ ```
28
+
29
+ Rebuild the native app after installing. A JavaScript reload is not enough.
30
+
31
+ ## Use
32
+
33
+ ```jsx
34
+ import {
35
+ FoldStateProvider,
36
+ useFoldState,
37
+ FOLD_STATE_EVENT,
38
+ FOLD_STATUS,
39
+ } from 'react-native-device-posture';
40
+
41
+ function App() {
42
+ return (
43
+ <FoldStateProvider>
44
+ <Root />
45
+ </FoldStateProvider>
46
+ );
47
+ }
48
+
49
+ function Home() {
50
+ const { isFoldSupported, isFlat, status } = useFoldState();
51
+ const isDuoOpen = isFoldSupported && isFlat;
52
+
53
+ return isDuoOpen ? <WideHome /> : <PhoneHome />;
54
+ }
55
+ ```
56
+
57
+ `FOLD_STATE_EVENT` is `'onFoldStateChange'` on both platforms. The provider already subscribes to it, so screens only need `useFoldState()`.
58
+
59
+ `useFoldState()` throws if it is called outside `FoldStateProvider`.
@@ -0,0 +1,62 @@
1
+ buildscript {
2
+ def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["DevicePosture_kotlinVersion"]
3
+
4
+ repositories {
5
+ google()
6
+ mavenCentral()
7
+ }
8
+
9
+ dependencies {
10
+ classpath "com.android.tools.build:gradle:8.7.2"
11
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
12
+ }
13
+ }
14
+
15
+ def isNewArchitectureEnabled() {
16
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
17
+ }
18
+
19
+ apply plugin: "com.android.library"
20
+ apply plugin: "kotlin-android"
21
+
22
+ if (isNewArchitectureEnabled()) {
23
+ apply plugin: "com.facebook.react"
24
+ }
25
+
26
+ def getExtOrDefault(name) {
27
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["DevicePosture_" + name]
28
+ }
29
+
30
+ def getExtOrIntegerDefault(name) {
31
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["DevicePosture_" + name]).toInteger()
32
+ }
33
+
34
+ android {
35
+ namespace "com.deviceposture"
36
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
37
+
38
+ defaultConfig {
39
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
40
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
41
+ }
42
+
43
+ compileOptions {
44
+ sourceCompatibility JavaVersion.VERSION_17
45
+ targetCompatibility JavaVersion.VERSION_17
46
+ }
47
+
48
+ kotlinOptions {
49
+ jvmTarget = "17"
50
+ }
51
+ }
52
+
53
+ repositories {
54
+ mavenCentral()
55
+ google()
56
+ }
57
+
58
+ dependencies {
59
+ implementation "com.facebook.react:react-android"
60
+ implementation "androidx.window:window:1.3.0"
61
+ implementation "androidx.window:window-java:1.3.0"
62
+ }
@@ -0,0 +1,4 @@
1
+ DevicePosture_kotlinVersion=1.9.24
2
+ DevicePosture_minSdkVersion=24
3
+ DevicePosture_targetSdkVersion=35
4
+ DevicePosture_compileSdkVersion=35
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,131 @@
1
+ package com.deviceposture
2
+
3
+ import android.content.pm.PackageManager
4
+ import android.os.Build
5
+ import androidx.core.util.Consumer
6
+ import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter
7
+ import androidx.window.layout.FoldingFeature
8
+ import androidx.window.layout.WindowInfoTracker
9
+ import androidx.window.layout.WindowLayoutInfo
10
+ import com.facebook.react.bridge.Arguments
11
+ import com.facebook.react.bridge.LifecycleEventListener
12
+ import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.facebook.react.bridge.WritableMap
14
+ import java.util.concurrent.ExecutorService
15
+ import java.util.concurrent.Executors
16
+
17
+ class DevicePostureModule(reactContext: ReactApplicationContext) :
18
+ NativeDevicePostureSpec(reactContext), LifecycleEventListener {
19
+
20
+ private data class Posture(
21
+ val isFoldSupported: Boolean,
22
+ val status: String,
23
+ val isFlat: Boolean,
24
+ )
25
+
26
+ private val executor: ExecutorService = Executors.newSingleThreadExecutor()
27
+ private var tracker: WindowInfoTrackerCallbackAdapter? = null
28
+ private var listening = false
29
+
30
+ @Volatile private var posture = Posture(false, "unknown", false)
31
+
32
+ private val layoutCallback =
33
+ Consumer<WindowLayoutInfo> { info ->
34
+ publish(info)
35
+ }
36
+
37
+ init {
38
+ if (hasHinge()) {
39
+ tracker = WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(reactContext))
40
+ posture = Posture(true, "unknown", false)
41
+ }
42
+ reactContext.addLifecycleEventListener(this)
43
+ }
44
+
45
+ override fun getName(): String = NAME
46
+
47
+ override fun getFoldState(): WritableMap = posture.toMap()
48
+
49
+ override fun startListening() {
50
+ listening = true
51
+ attachListener()
52
+ }
53
+
54
+ override fun stopListening() {
55
+ listening = false
56
+ detachListener()
57
+ }
58
+
59
+ override fun onHostResume() {
60
+ if (listening) {
61
+ attachListener()
62
+ }
63
+ }
64
+
65
+ override fun onHostPause() = Unit
66
+
67
+ override fun onHostDestroy() {
68
+ stopListening()
69
+ reactApplicationContext.removeLifecycleEventListener(this)
70
+ executor.shutdown()
71
+ }
72
+
73
+ private fun attachListener() {
74
+ val activity = reactApplicationContext.currentActivity
75
+ val windowTracker = tracker
76
+ if (!listening || activity == null || windowTracker == null) {
77
+ return
78
+ }
79
+ windowTracker.removeWindowLayoutInfoListener(layoutCallback)
80
+ windowTracker.addWindowLayoutInfoListener(activity, executor, layoutCallback)
81
+ }
82
+
83
+ private fun detachListener() {
84
+ tracker?.removeWindowLayoutInfoListener(layoutCallback)
85
+ }
86
+
87
+ private fun publish(info: WindowLayoutInfo) {
88
+ val supported = hasHinge()
89
+ val foldingFeature = info.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
90
+ val next =
91
+ when {
92
+ !supported -> Posture(false, "unknown", false)
93
+ foldingFeature == null -> Posture(true, "closed", false)
94
+ foldingFeature.state == FoldingFeature.State.FLAT -> Posture(true, "fullyOpen", true)
95
+ foldingFeature.state == FoldingFeature.State.HALF_OPENED ->
96
+ Posture(true, "partiallyOpen", false)
97
+ else -> Posture(true, "unknown", false)
98
+ }
99
+
100
+ if (next == posture) {
101
+ return
102
+ }
103
+ posture = next
104
+ val snapshot = next
105
+ reactApplicationContext.runOnUiQueueThread {
106
+ emitOnFoldStateChange(snapshot.toMap())
107
+ }
108
+ }
109
+
110
+ private fun hasHinge(): Boolean {
111
+ val feature =
112
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
113
+ PackageManager.FEATURE_SENSOR_HINGE_ANGLE
114
+ } else {
115
+ "android.hardware.sensor.hinge_angle"
116
+ }
117
+ return reactApplicationContext.packageManager.hasSystemFeature(feature)
118
+ }
119
+
120
+ private fun Posture.toMap(): WritableMap {
121
+ return Arguments.createMap().apply {
122
+ putBoolean("isFoldSupported", isFoldSupported)
123
+ putString("status", status)
124
+ putBoolean("isFlat", isFlat)
125
+ }
126
+ }
127
+
128
+ companion object {
129
+ const val NAME = "NativeDevicePosture"
130
+ }
131
+ }
@@ -0,0 +1,32 @@
1
+ package com.deviceposture
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ class DevicePosturePackage : BaseReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11
+ return if (name == DevicePostureModule.NAME) {
12
+ DevicePostureModule(reactContext)
13
+ } else {
14
+ null
15
+ }
16
+ }
17
+
18
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
19
+ return ReactModuleInfoProvider {
20
+ mapOf(
21
+ DevicePostureModule.NAME to ReactModuleInfo(
22
+ DevicePostureModule.NAME,
23
+ DevicePostureModule::class.java.name,
24
+ false,
25
+ true,
26
+ false,
27
+ true
28
+ )
29
+ )
30
+ }
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ #import <Foundation/Foundation.h>
@@ -0,0 +1,164 @@
1
+ #import <RNDevicePostureSpec/RNDevicePostureSpec.h>
2
+ #import <UIKit/UIHinge.h>
3
+ #import <UIKit/UIHingeInteraction.h>
4
+ #import <objc/runtime.h>
5
+
6
+ @class DevicePostureModule;
7
+
8
+ static NSDictionary *DevicePostureCurrentState;
9
+ static __weak DevicePostureModule *DevicePostureInstance;
10
+ static const void *DevicePostureInteractionKey = &DevicePostureInteractionKey;
11
+
12
+ static NSDictionary *DevicePostureState(BOOL isFoldSupported, NSString *status, BOOL isFlat)
13
+ {
14
+ return @{
15
+ @"isFoldSupported" : @(isFoldSupported),
16
+ @"status" : status ?: @"unknown",
17
+ @"isFlat" : @(isFlat),
18
+ };
19
+ }
20
+
21
+ static UIView *DevicePostureRootView(void)
22
+ {
23
+ for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
24
+ if (![scene isKindOfClass:[UIWindowScene class]]) {
25
+ continue;
26
+ }
27
+ UIWindowScene *windowScene = (UIWindowScene *)scene;
28
+ for (UIWindow *window in windowScene.windows) {
29
+ if (window.isKeyWindow && window.rootViewController.view != nil) {
30
+ return window.rootViewController.view;
31
+ }
32
+ }
33
+ }
34
+ return nil;
35
+ }
36
+
37
+ @interface DevicePostureModule : NativeDevicePostureSpecBase <NativeDevicePostureSpec>
38
+ + (void)applySupported:(BOOL)isFoldSupported status:(NSString *)status isFlat:(BOOL)isFlat;
39
+ - (void)attachHingeObserver;
40
+ - (void)detachHingeObserver;
41
+ @end
42
+
43
+ @implementation DevicePostureModule
44
+
45
+ RCT_EXPORT_MODULE(NativeDevicePosture);
46
+
47
+ + (void)initialize
48
+ {
49
+ if (self == [DevicePostureModule class] && DevicePostureCurrentState == nil) {
50
+ DevicePostureCurrentState = DevicePostureState(NO, @"unknown", NO);
51
+ }
52
+ }
53
+
54
+ + (BOOL)requiresMainQueueSetup
55
+ {
56
+ return YES;
57
+ }
58
+
59
+ - (instancetype)init
60
+ {
61
+ if (self = [super init]) {
62
+ DevicePostureInstance = self;
63
+ }
64
+ return self;
65
+ }
66
+
67
+ - (void)startListening
68
+ {
69
+ dispatch_async(dispatch_get_main_queue(), ^{
70
+ [self attachHingeObserver];
71
+ });
72
+ }
73
+
74
+ - (void)stopListening
75
+ {
76
+ dispatch_async(dispatch_get_main_queue(), ^{
77
+ [self detachHingeObserver];
78
+ });
79
+ }
80
+
81
+ - (void)attachHingeObserver
82
+ {
83
+ if (@available(iOS 27.1, *)) {
84
+ UIView *view = DevicePostureRootView();
85
+ if (view == nil || objc_getAssociatedObject(view, DevicePostureInteractionKey) != nil) {
86
+ return;
87
+ }
88
+
89
+ UIHingeInteraction *interaction = [[UIHingeInteraction alloc]
90
+ initWithUpdateHandler:^(__unused UIHingeInteraction *hingeInteraction,
91
+ UIHingeInteractionUpdate *update) {
92
+ UIHinge *hinge = update.hinge;
93
+ if (hinge == nil) {
94
+ [DevicePostureModule applySupported:NO status:@"unknown" isFlat:NO];
95
+ return;
96
+ }
97
+
98
+ switch (hinge.status) {
99
+ case UIHingeStatusClosed:
100
+ [DevicePostureModule applySupported:YES status:@"closed" isFlat:NO];
101
+ break;
102
+ case UIHingeStatusPartiallyOpen:
103
+ [DevicePostureModule applySupported:YES status:@"partiallyOpen" isFlat:NO];
104
+ break;
105
+ case UIHingeStatusFullyOpen:
106
+ [DevicePostureModule applySupported:YES status:@"fullyOpen" isFlat:YES];
107
+ break;
108
+ default:
109
+ [DevicePostureModule applySupported:YES status:@"unknown" isFlat:NO];
110
+ break;
111
+ }
112
+ }];
113
+
114
+ objc_setAssociatedObject(
115
+ view, DevicePostureInteractionKey, interaction, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
116
+ [view addInteraction:interaction];
117
+ }
118
+ }
119
+
120
+ - (void)detachHingeObserver
121
+ {
122
+ if (@available(iOS 27.1, *)) {
123
+ UIView *view = DevicePostureRootView();
124
+ id interaction = view ? objc_getAssociatedObject(view, DevicePostureInteractionKey) : nil;
125
+ if (view != nil && interaction != nil) {
126
+ [view removeInteraction:interaction];
127
+ objc_setAssociatedObject(view, DevicePostureInteractionKey, nil, OBJC_ASSOCIATION_ASSIGN);
128
+ }
129
+ }
130
+ }
131
+
132
+ + (void)applySupported:(BOOL)isFoldSupported status:(NSString *)status isFlat:(BOOL)isFlat
133
+ {
134
+ void (^apply)(void) = ^{
135
+ NSDictionary *next = DevicePostureState(isFoldSupported, status, isFlat);
136
+ if ([DevicePostureCurrentState isEqualToDictionary:next]) {
137
+ return;
138
+ }
139
+ DevicePostureCurrentState = next;
140
+ DevicePostureModule *module = DevicePostureInstance;
141
+ if (module != nil) {
142
+ [module emitOnFoldStateChange:next];
143
+ }
144
+ };
145
+
146
+ if ([NSThread isMainThread]) {
147
+ apply();
148
+ } else {
149
+ dispatch_async(dispatch_get_main_queue(), apply);
150
+ }
151
+ }
152
+
153
+ - (NSDictionary *)getFoldState
154
+ {
155
+ return DevicePostureCurrentState ?: DevicePostureState(NO, @"unknown", NO);
156
+ }
157
+
158
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
159
+ (const facebook::react::ObjCTurboModule::InitParams &)params
160
+ {
161
+ return std::make_shared<facebook::react::NativeDevicePostureSpecJSI>(params);
162
+ }
163
+
164
+ @end
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "react-native-device-posture",
3
+ "version": "0.1.0",
4
+ "description": "Shared fold and hinge posture for iOS and Android, available through one provider.",
5
+ "main": "src/index.js",
6
+ "react-native": "src/index.js",
7
+ "files": [
8
+ "src",
9
+ "ios",
10
+ "android",
11
+ "react-native-device-posture.podspec",
12
+ "react-native.config.js",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test __tests__/normalizeFoldState.test.js"
17
+ },
18
+ "keywords": [
19
+ "react-native",
20
+ "fold",
21
+ "hinge",
22
+ "device-posture",
23
+ "iphone-duo",
24
+ "foldable"
25
+ ],
26
+
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "react": ">=18",
30
+ "react-native": ">=0.76"
31
+ },
32
+ "codegenConfig": {
33
+ "name": "RNDevicePostureSpec",
34
+ "type": "modules",
35
+ "jsSrcsDir": "src",
36
+ "android": {
37
+ "javaPackageName": "com.deviceposture"
38
+ }
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
@@ -0,0 +1,35 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+ folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5
+
6
+ Pod::Spec.new do |s|
7
+ s.name = "react-native-device-posture"
8
+ s.version = package["version"]
9
+ s.summary = package["description"]
10
+ s.homepage = "https://github.com/device-posture/react-native-device-posture"
11
+ s.license = "MIT"
12
+ s.authors = "device-posture"
13
+ s.platforms = { :ios => "15.1" }
14
+ s.source = { :git => "https://github.com/device-posture/react-native-device-posture.git", :tag => "#{s.version}" }
15
+ s.source_files = "ios/**/*.{h,m,mm}"
16
+
17
+ if respond_to?(:install_modules_dependencies, true)
18
+ install_modules_dependencies(s)
19
+ else
20
+ s.dependency "React-Core"
21
+ if ENV["RCT_NEW_ARCH_ENABLED"] == "1"
22
+ s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
23
+ s.pod_target_xcconfig = {
24
+ "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
25
+ "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
26
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20"
27
+ }
28
+ s.dependency "React-Codegen"
29
+ s.dependency "RCT-Folly"
30
+ s.dependency "RCTRequired"
31
+ s.dependency "RCTTypeSafety"
32
+ s.dependency "ReactCommon/turbomodule/core"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ ios: {},
5
+ android: {},
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,69 @@
1
+ const React = require('react');
2
+ const { FOLD_STATE_EVENT } = require('./constants');
3
+ const { EMPTY_FOLD_STATE, normalizeFoldState } = require('./normalizeFoldState');
4
+
5
+ const FoldStateContext = React.createContext(null);
6
+
7
+ function readInitialState() {
8
+ let NativeDevicePosture;
9
+ try {
10
+ NativeDevicePosture = require('./NativeDevicePosture').default;
11
+ } catch (error) {
12
+ return EMPTY_FOLD_STATE;
13
+ }
14
+
15
+ if (!NativeDevicePosture || typeof NativeDevicePosture.getFoldState !== 'function') {
16
+ return EMPTY_FOLD_STATE;
17
+ }
18
+
19
+ return normalizeFoldState(NativeDevicePosture.getFoldState());
20
+ }
21
+
22
+ function FoldStateProvider({ children }) {
23
+ const [foldState, setFoldState] = React.useState(readInitialState);
24
+
25
+ React.useEffect(() => {
26
+ let NativeDevicePosture;
27
+ try {
28
+ NativeDevicePosture = require('./NativeDevicePosture').default;
29
+ } catch (error) {
30
+ return undefined;
31
+ }
32
+
33
+ const listen = NativeDevicePosture && NativeDevicePosture[FOLD_STATE_EVENT];
34
+ if (!NativeDevicePosture || typeof listen !== 'function') {
35
+ return undefined;
36
+ }
37
+
38
+ NativeDevicePosture.startListening();
39
+ const subscription = listen((next) => {
40
+ setFoldState(normalizeFoldState(next));
41
+ });
42
+
43
+ return () => {
44
+ subscription?.remove?.();
45
+ NativeDevicePosture.stopListening?.();
46
+ };
47
+ }, []);
48
+
49
+ return React.createElement(
50
+ FoldStateContext.Provider,
51
+ { value: foldState },
52
+ children
53
+ );
54
+ }
55
+
56
+ function useFoldState() {
57
+ const foldState = React.useContext(FoldStateContext);
58
+ if (foldState == null) {
59
+ throw new Error(
60
+ 'useFoldState must be used within FoldStateProvider'
61
+ );
62
+ }
63
+ return foldState;
64
+ }
65
+
66
+ module.exports = {
67
+ FoldStateProvider,
68
+ useFoldState,
69
+ };
@@ -0,0 +1,21 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+ import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
4
+
5
+ export type FoldState = {
6
+ isFoldSupported: boolean;
7
+ status: string;
8
+ isFlat: boolean;
9
+ };
10
+
11
+ export interface Spec extends TurboModule {
12
+ getFoldState(): FoldState;
13
+ startListening(): void;
14
+ stopListening(): void;
15
+ /**
16
+ * Shared listener for iOS and Android. The property name is `onFoldStateChange`.
17
+ */
18
+ readonly onFoldStateChange: EventEmitter<FoldState>;
19
+ }
20
+
21
+ export default TurboModuleRegistry.get<Spec>('NativeDevicePosture');
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Same listener name on iOS and Android.
3
+ * Subscribe with `nativeModule[FOLD_STATE_EVENT](handler)`.
4
+ */
5
+ const FOLD_STATE_EVENT = 'onFoldStateChange';
6
+
7
+ const FOLD_STATUS = {
8
+ UNKNOWN: 'unknown',
9
+ CLOSED: 'closed',
10
+ PARTIALLY_OPEN: 'partiallyOpen',
11
+ FULLY_OPEN: 'fullyOpen',
12
+ };
13
+
14
+ module.exports = {
15
+ FOLD_STATE_EVENT,
16
+ FOLD_STATUS,
17
+ };
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ const { FOLD_STATE_EVENT, FOLD_STATUS } = require('./constants');
2
+ const { EMPTY_FOLD_STATE, normalizeFoldState } = require('./normalizeFoldState');
3
+ const { FoldStateProvider, useFoldState } = require('./FoldStateProvider');
4
+
5
+ module.exports = {
6
+ FOLD_STATE_EVENT,
7
+ FOLD_STATUS,
8
+ EMPTY_FOLD_STATE,
9
+ normalizeFoldState,
10
+ FoldStateProvider,
11
+ useFoldState,
12
+ };
@@ -0,0 +1,26 @@
1
+ const { FOLD_STATUS } = require('./constants');
2
+
3
+ const KNOWN_STATUSES = new Set(Object.values(FOLD_STATUS));
4
+
5
+ const EMPTY_FOLD_STATE = {
6
+ isFoldSupported: false,
7
+ status: FOLD_STATUS.UNKNOWN,
8
+ isFlat: false,
9
+ };
10
+
11
+ function normalizeFoldState(raw) {
12
+ const isFoldSupported = Boolean(raw && raw.isFoldSupported);
13
+ const status =
14
+ raw && KNOWN_STATUSES.has(raw.status) ? raw.status : FOLD_STATUS.UNKNOWN;
15
+
16
+ return {
17
+ isFoldSupported,
18
+ status: isFoldSupported ? status : FOLD_STATUS.UNKNOWN,
19
+ isFlat: isFoldSupported && status === FOLD_STATUS.FULLY_OPEN,
20
+ };
21
+ }
22
+
23
+ module.exports = {
24
+ EMPTY_FOLD_STATE,
25
+ normalizeFoldState,
26
+ };