@rn-tools/sheets 0.1.4 → 3.0.2
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/CHANGELOG.md +16 -0
- package/README.md +107 -67
- package/android/src/main/java/expo/modules/sheets/RNToolsSheetsModule.kt +8 -4
- package/android/src/main/java/expo/modules/sheets/RNToolsSheetsView.kt +110 -76
- package/android/src/main/java/expo/modules/sheets/SheetProps.kt +2 -1
- package/ios/RNToolsSheets.podspec +3 -3
- package/ios/RNToolsSheetsModule.swift +27 -10
- package/ios/RNToolsSheetsView.swift +269 -224
- package/ios/Sources/RNToolsTouchHandlerHelper.h +15 -0
- package/ios/Sources/RNToolsTouchHandlerHelper.mm +31 -0
- package/mocks/expo-modules-core.mock.ts +9 -0
- package/package.json +10 -14
- package/src/index.ts +4 -1
- package/src/native-sheets-view.tsx +126 -42
- package/src/sheet-slot.tsx +70 -0
- package/src/sheets-client.test.tsx +239 -0
- package/src/sheets-client.tsx +233 -0
- package/src/sheets-provider.tsx +20 -0
- package/vitest.config.mts +25 -0
- package/ios/Sources/RNTSurfaceTouchHandlerWrapper.h +0 -11
- package/ios/Sources/RNTSurfaceTouchHandlerWrapper.mm +0 -43
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createStore } from "@rn-tools/core";
|
|
3
|
+
import type { Store } from "@rn-tools/core";
|
|
4
|
+
import type {
|
|
5
|
+
AppearanceAndroid,
|
|
6
|
+
AppearanceIOS,
|
|
7
|
+
SheetChangeEvent,
|
|
8
|
+
} from "./native-sheets-view";
|
|
9
|
+
import type { ViewStyle } from "react-native";
|
|
10
|
+
|
|
11
|
+
export type SheetOptions = {
|
|
12
|
+
id?: string;
|
|
13
|
+
snapPoints?: number[];
|
|
14
|
+
initialIndex?: number;
|
|
15
|
+
canDismiss?: boolean;
|
|
16
|
+
onDismissPrevented?: () => void;
|
|
17
|
+
onStateChange?: (event: SheetChangeEvent) => void;
|
|
18
|
+
containerStyle?: ViewStyle;
|
|
19
|
+
appearanceAndroid?: AppearanceAndroid;
|
|
20
|
+
appearanceIOS?: AppearanceIOS;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type SheetStatus = "opening" | "open" | "closing";
|
|
24
|
+
|
|
25
|
+
export type SheetEntry = {
|
|
26
|
+
key: string;
|
|
27
|
+
element: React.ReactElement;
|
|
28
|
+
options: SheetOptions;
|
|
29
|
+
status: SheetStatus;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type SheetsState = {
|
|
33
|
+
sheets: SheetEntry[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type SheetsStore = Store<SheetsState>;
|
|
37
|
+
|
|
38
|
+
export type SheetsClient = {
|
|
39
|
+
store: SheetsStore;
|
|
40
|
+
present: (element: React.ReactElement, options?: SheetOptions) => string;
|
|
41
|
+
dismiss: (id?: string) => void;
|
|
42
|
+
dismissAll: () => void;
|
|
43
|
+
remove: (id: string) => void;
|
|
44
|
+
markDidOpen: (key: string) => void;
|
|
45
|
+
markDidDismiss: (key: string) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const SheetsContext = React.createContext<SheetsClient | null>(null);
|
|
49
|
+
export const SheetsStoreContext = React.createContext<SheetsStore | null>(null);
|
|
50
|
+
|
|
51
|
+
let counter = 0;
|
|
52
|
+
|
|
53
|
+
export function createSheets(): SheetsClient {
|
|
54
|
+
const store = createStore<SheetsState>({ sheets: [] });
|
|
55
|
+
|
|
56
|
+
function present(
|
|
57
|
+
element: React.ReactElement,
|
|
58
|
+
options: SheetOptions = {},
|
|
59
|
+
): string {
|
|
60
|
+
const generatedKey = `sheet-${++counter}`;
|
|
61
|
+
let presentedKey = generatedKey;
|
|
62
|
+
|
|
63
|
+
store.setState((prev) => {
|
|
64
|
+
if (options.id == null) {
|
|
65
|
+
return {
|
|
66
|
+
...prev,
|
|
67
|
+
sheets: [
|
|
68
|
+
...prev.sheets,
|
|
69
|
+
{ key: generatedKey, element, options, status: "opening" },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const duplicateIndex = prev.sheets.findIndex(
|
|
75
|
+
(entry) => entry.options.id === options.id,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (duplicateIndex === -1) {
|
|
79
|
+
return {
|
|
80
|
+
...prev,
|
|
81
|
+
sheets: [
|
|
82
|
+
...prev.sheets,
|
|
83
|
+
{ key: generatedKey, element, options, status: "opening" },
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const duplicate = prev.sheets[duplicateIndex];
|
|
89
|
+
presentedKey = duplicate.key;
|
|
90
|
+
const nextEntry: SheetEntry = {
|
|
91
|
+
key: duplicate.key,
|
|
92
|
+
element,
|
|
93
|
+
options,
|
|
94
|
+
status: "opening",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const withoutDuplicate = prev.sheets.filter((_, i) => i !== duplicateIndex);
|
|
98
|
+
return {
|
|
99
|
+
...prev,
|
|
100
|
+
sheets: [...withoutDuplicate, nextEntry],
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return presentedKey;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dismiss(id?: string) {
|
|
108
|
+
store.setState((prev) => {
|
|
109
|
+
if (prev.sheets.length === 0) return prev;
|
|
110
|
+
|
|
111
|
+
let targetIndex = -1;
|
|
112
|
+
|
|
113
|
+
if (id == null) {
|
|
114
|
+
for (let i = prev.sheets.length - 1; i >= 0; i--) {
|
|
115
|
+
if (prev.sheets[i].status !== "closing") {
|
|
116
|
+
targetIndex = i;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
targetIndex = prev.sheets.findIndex(
|
|
122
|
+
(entry) => entry.options.id === id || entry.key === id,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (targetIndex === -1) return prev;
|
|
127
|
+
|
|
128
|
+
const entry = prev.sheets[targetIndex];
|
|
129
|
+
if (entry.status === "closing") return prev;
|
|
130
|
+
|
|
131
|
+
const sheets = [...prev.sheets];
|
|
132
|
+
sheets[targetIndex] = { ...entry, status: "closing" };
|
|
133
|
+
return { ...prev, sheets };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function dismissAll() {
|
|
139
|
+
store.setState((prev) => {
|
|
140
|
+
if (prev.sheets.length === 0) return prev;
|
|
141
|
+
|
|
142
|
+
let changed = false;
|
|
143
|
+
const sheets = prev.sheets.map((entry) => {
|
|
144
|
+
if (entry.status === "closing") return entry;
|
|
145
|
+
changed = true;
|
|
146
|
+
return { ...entry, status: "closing" as const };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!changed) return prev;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...prev,
|
|
153
|
+
sheets,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function remove(id: string) {
|
|
160
|
+
store.setState((prev) => {
|
|
161
|
+
const targetIndex = prev.sheets.findIndex(
|
|
162
|
+
(entry) => entry.options.id === id || entry.key === id,
|
|
163
|
+
);
|
|
164
|
+
if (targetIndex === -1) return prev;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
...prev,
|
|
168
|
+
sheets: prev.sheets.filter((_, i) => i !== targetIndex),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function markDidOpen(key: string) {
|
|
175
|
+
store.setState((prev) => {
|
|
176
|
+
const index = prev.sheets.findIndex((entry) => entry.key === key);
|
|
177
|
+
if (index === -1) return prev;
|
|
178
|
+
|
|
179
|
+
const entry = prev.sheets[index];
|
|
180
|
+
if (entry.status !== "opening") return prev;
|
|
181
|
+
|
|
182
|
+
const sheets = [...prev.sheets];
|
|
183
|
+
sheets[index] = { ...entry, status: "open" };
|
|
184
|
+
return { ...prev, sheets };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function markDidDismiss(key: string) {
|
|
190
|
+
store.setState((prev) => {
|
|
191
|
+
const index = prev.sheets.findIndex((entry) => entry.key === key);
|
|
192
|
+
if (index === -1) return prev;
|
|
193
|
+
|
|
194
|
+
const entry = prev.sheets[index];
|
|
195
|
+
if (entry.status !== "closing") {
|
|
196
|
+
// Ignore dismiss notifications unless close was requested.
|
|
197
|
+
return prev;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
...prev,
|
|
202
|
+
sheets: prev.sheets.filter((_, i) => i !== index),
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
store,
|
|
210
|
+
present,
|
|
211
|
+
dismiss,
|
|
212
|
+
dismissAll,
|
|
213
|
+
remove,
|
|
214
|
+
markDidOpen,
|
|
215
|
+
markDidDismiss,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function useSheets(): SheetsClient {
|
|
220
|
+
const sheets = React.useContext(SheetsContext);
|
|
221
|
+
if (!sheets) {
|
|
222
|
+
throw new Error("SheetsProvider is missing from the component tree.");
|
|
223
|
+
}
|
|
224
|
+
return sheets;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function useSheetsStore(): SheetsStore {
|
|
228
|
+
const store = React.useContext(SheetsStoreContext);
|
|
229
|
+
if (!store) {
|
|
230
|
+
throw new Error("SheetsProvider is missing from the component tree.");
|
|
231
|
+
}
|
|
232
|
+
return store;
|
|
233
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { SheetsContext, SheetsStoreContext } from "./sheets-client";
|
|
3
|
+
import type { SheetsClient } from "./sheets-client";
|
|
4
|
+
import { SheetSlot } from "./sheet-slot";
|
|
5
|
+
|
|
6
|
+
export type SheetsProviderProps = {
|
|
7
|
+
sheets: SheetsClient;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function SheetsProvider({ sheets, children }: SheetsProviderProps) {
|
|
12
|
+
return (
|
|
13
|
+
<SheetsContext.Provider value={sheets}>
|
|
14
|
+
<SheetsStoreContext.Provider value={sheets.store}>
|
|
15
|
+
{children}
|
|
16
|
+
<SheetSlot />
|
|
17
|
+
</SheetsStoreContext.Provider>
|
|
18
|
+
</SheetsContext.Provider>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { defineConfig } from "vitest/config";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
"expo-modules-core": path.resolve(
|
|
11
|
+
__dirname,
|
|
12
|
+
"mocks/expo-modules-core.mock.ts",
|
|
13
|
+
),
|
|
14
|
+
"react-native": path.resolve(
|
|
15
|
+
__dirname,
|
|
16
|
+
"../core/mocks/react-native.mock.ts",
|
|
17
|
+
),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
test: {
|
|
21
|
+
environment: "jsdom",
|
|
22
|
+
globals: true,
|
|
23
|
+
setupFiles: [path.resolve(__dirname, "../core/mocks/setup.ts")],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// RNTSurfaceTouchHandlerWrapper.mm (Objective‑C++, .mm extension!)
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
#import "RNTSurfaceTouchHandlerWrapper.h"
|
|
5
|
-
#if RCT_NEW_ARCH_ENABLED
|
|
6
|
-
#import <React/RCTSurfaceTouchHandler.h>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@implementation RNTSurfaceTouchHandlerWrapper {
|
|
10
|
-
|
|
11
|
-
RCTSurfaceTouchHandler *_handler;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
- (instancetype)init {
|
|
15
|
-
if (self = [super init]) {
|
|
16
|
-
_handler = [[RCTSurfaceTouchHandler alloc] init];
|
|
17
|
-
}
|
|
18
|
-
return self;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
- (void)attachToView:(UIView *)view {
|
|
22
|
-
[_handler attachToView:view];
|
|
23
|
-
}
|
|
24
|
-
@end
|
|
25
|
-
#else
|
|
26
|
-
|
|
27
|
-
@implementation RNTSurfaceTouchHandlerWrapper {
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
- (instancetype)init {
|
|
31
|
-
if (self = [super init]) {
|
|
32
|
-
}
|
|
33
|
-
return self;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
- (void)attachToView:(UIView *)view {
|
|
37
|
-
}
|
|
38
|
-
@end
|
|
39
|
-
|
|
40
|
-
#endif
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|