@storybook/react-native-ui-lite 9.0.0-beta.11

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +94 -0
  3. package/dist/index.js +5437 -0
  4. package/package.json +80 -0
  5. package/src/Button.stories.tsx +134 -0
  6. package/src/Button.tsx +172 -0
  7. package/src/Explorer.stories.tsx +40 -0
  8. package/src/Explorer.tsx +38 -0
  9. package/src/IconButton.tsx +10 -0
  10. package/src/Layout.stories.tsx +70 -0
  11. package/src/Layout.tsx +310 -0
  12. package/src/LayoutProvider.tsx +32 -0
  13. package/src/MobileAddonsPanel.tsx +195 -0
  14. package/src/MobileMenuDrawer.tsx +90 -0
  15. package/src/Refs.tsx +82 -0
  16. package/src/Search.tsx +234 -0
  17. package/src/SearchResults.stories.tsx +102 -0
  18. package/src/SearchResults.tsx +254 -0
  19. package/src/SelectedNodeProvider.tsx +58 -0
  20. package/src/Sidebar.stories.tsx +188 -0
  21. package/src/Sidebar.tsx +131 -0
  22. package/src/StorageProvider.tsx +21 -0
  23. package/src/StorybookLogo.stories.tsx +76 -0
  24. package/src/StorybookLogo.tsx +108 -0
  25. package/src/Tree.stories.tsx +177 -0
  26. package/src/Tree.tsx +390 -0
  27. package/src/TreeNode.stories.tsx +117 -0
  28. package/src/TreeNode.tsx +154 -0
  29. package/src/assets/react-native-logo.png +0 -0
  30. package/src/constants.ts +4 -0
  31. package/src/hooks/useExpanded.ts +64 -0
  32. package/src/hooks/useLastViewed.ts +48 -0
  33. package/src/hooks/useStoreState.ts +27 -0
  34. package/src/icon/iconDataUris.tsx +365 -0
  35. package/src/index.tsx +11 -0
  36. package/src/mockdata.large.ts +25217 -0
  37. package/src/mockdata.ts +287 -0
  38. package/src/types.ts +66 -0
  39. package/src/util/StoryHash.ts +249 -0
  40. package/src/util/status.tsx +87 -0
  41. package/src/util/tree.ts +93 -0
  42. package/src/util/useStyle.ts +28 -0
package/src/Search.tsx ADDED
@@ -0,0 +1,234 @@
1
+ import { styled } from '@storybook/react-native-theming';
2
+ import type { IFuseOptions } from 'fuse.js';
3
+ import Fuse from 'fuse.js';
4
+ import React, { useCallback, useDeferredValue, useRef, useState } from 'react';
5
+ import { TextInput, View } from 'react-native';
6
+ import { useSelectedNode } from './SelectedNodeProvider';
7
+ import {
8
+ type CombinedDataset,
9
+ type GetSearchItemProps,
10
+ isExpandType,
11
+ type SearchChildrenFn,
12
+ type SearchItem,
13
+ type SearchResult,
14
+ type Selection,
15
+ } from './types';
16
+ import { searchItem } from './util/tree';
17
+ import { CloseIcon, SearchIcon } from './icon/iconDataUris';
18
+
19
+ const DEFAULT_MAX_SEARCH_RESULTS = 50;
20
+
21
+ const options = {
22
+ shouldSort: true,
23
+ tokenize: true,
24
+ findAllMatches: true,
25
+ includeScore: true,
26
+ includeMatches: true,
27
+ threshold: 0.2,
28
+ location: 0,
29
+ distance: 100,
30
+ maxPatternLength: 32,
31
+ minMatchCharLength: 1,
32
+ keys: [
33
+ { name: 'name', weight: 0.7 },
34
+ { name: 'path', weight: 0.3 },
35
+ ],
36
+ } as IFuseOptions<SearchItem>;
37
+
38
+ const SearchIconWrapper = styled.View({
39
+ position: 'absolute',
40
+ top: 0,
41
+ left: 8,
42
+ zIndex: 1,
43
+ pointerEvents: 'none',
44
+ display: 'flex',
45
+ flexDirection: 'row',
46
+ alignItems: 'center',
47
+ height: '100%',
48
+ });
49
+
50
+ const SearchField = styled.View({
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ position: 'relative',
54
+ });
55
+
56
+ const Input = styled(TextInput)(({ theme }) => ({
57
+ height: 32,
58
+ paddingLeft: 28,
59
+ paddingRight: 28,
60
+ borderWidth: 1,
61
+ borderColor: theme.appBorderColor,
62
+ backgroundColor: 'transparent',
63
+ borderRadius: 6,
64
+ fontSize: theme.typography.size.s1 + 1,
65
+ color: theme.color.defaultText,
66
+ width: '100%',
67
+ }));
68
+
69
+ const ClearIcon = styled.TouchableOpacity(({ theme }) => ({
70
+ position: 'absolute',
71
+ top: 0,
72
+ bottom: 0,
73
+ right: 8,
74
+ zIndex: 1,
75
+ color: theme.textMutedColor,
76
+ cursor: 'pointer',
77
+ display: 'flex',
78
+ alignItems: 'center',
79
+ justifyContent: 'center',
80
+ height: '100%',
81
+ }));
82
+
83
+ export const Search = React.memo<{
84
+ children: SearchChildrenFn;
85
+ dataset: CombinedDataset;
86
+ setSelection: (selection: Selection) => void;
87
+ getLastViewed: () => Selection[];
88
+ initialQuery?: string;
89
+ }>(function Search({ children, dataset, setSelection, getLastViewed, initialQuery = '' }) {
90
+ const inputRef = useRef<TextInput>(null);
91
+ const [inputValue, setInputValue] = useState(initialQuery);
92
+ const [isOpen, setIsOpen] = useState(false);
93
+ const [allComponents, showAllComponents] = useState(false);
94
+ // const { isMobile } = useLayout();
95
+ const { scrollToSelectedNode } = useSelectedNode();
96
+
97
+ const selectStory = useCallback(
98
+ (id: string, refId: string) => {
99
+ setSelection({ storyId: id, refId });
100
+
101
+ inputRef.current?.blur();
102
+
103
+ setIsOpen(false);
104
+
105
+ showAllComponents(false);
106
+
107
+ scrollToSelectedNode();
108
+ },
109
+ [scrollToSelectedNode, setSelection]
110
+ );
111
+
112
+ const getItemProps: GetSearchItemProps = useCallback(
113
+ ({ item: result }) => {
114
+ return {
115
+ icon: result?.item?.type === 'component' ? 'component' : 'story',
116
+ result,
117
+ onPress: () => {
118
+ if (result?.item?.type === 'story') {
119
+ selectStory(result.item.id, result.item.refId);
120
+ } else if (result?.item?.type === 'component') {
121
+ selectStory(result.item.children[0], result.item.refId);
122
+ } else if (isExpandType(result) && result.showAll) {
123
+ result.showAll();
124
+ }
125
+ },
126
+ score: result.score,
127
+ refIndex: result.refIndex,
128
+ item: result.item,
129
+ matches: result.matches,
130
+ isHighlighted: false,
131
+ };
132
+ },
133
+ [selectStory]
134
+ );
135
+
136
+ const makeFuse = useCallback(() => {
137
+ const list = dataset.entries.reduce<SearchItem[]>((acc, [refId, { index }]) => {
138
+ if (index) {
139
+ acc.push(
140
+ ...Object.values(index).map((item) => {
141
+ return searchItem(item, dataset.hash[refId]);
142
+ })
143
+ );
144
+ }
145
+ return acc;
146
+ }, []);
147
+ return new Fuse(list, options);
148
+ }, [dataset]);
149
+
150
+ const getResults = useCallback(
151
+ (input: string) => {
152
+ const fuse = makeFuse();
153
+ if (!input) return [];
154
+
155
+ let results = [];
156
+ const resultIds: Set<string> = new Set();
157
+ const distinctResults = (fuse.search(input) as SearchResult[]).filter(({ item }) => {
158
+ if (
159
+ !(item.type === 'component' || item.type === 'docs' || item.type === 'story') ||
160
+ resultIds.has(item.parent)
161
+ ) {
162
+ return false;
163
+ }
164
+ resultIds.add(item.id);
165
+ return true;
166
+ });
167
+
168
+ if (distinctResults.length) {
169
+ results = distinctResults.slice(0, allComponents ? 1000 : DEFAULT_MAX_SEARCH_RESULTS);
170
+ if (distinctResults.length > DEFAULT_MAX_SEARCH_RESULTS && !allComponents) {
171
+ results.push({
172
+ showAll: () => showAllComponents(true),
173
+ totalCount: distinctResults.length,
174
+ moreCount: distinctResults.length - DEFAULT_MAX_SEARCH_RESULTS,
175
+ });
176
+ }
177
+ }
178
+
179
+ const lastViewed = !input && getLastViewed();
180
+ if (lastViewed && lastViewed.length) {
181
+ results = lastViewed.reduce((acc, { storyId, refId }) => {
182
+ const data = dataset.hash[refId];
183
+ if (data && data.index && data.index[storyId]) {
184
+ const story = data.index[storyId];
185
+ const item = story.type === 'story' ? data.index[story.parent] : story;
186
+ // prevent duplicates
187
+ if (!acc.some((res) => res.item.refId === refId && res.item.id === item.id)) {
188
+ acc.push({ item: searchItem(item, dataset.hash[refId]), matches: [], score: 0 });
189
+ }
190
+ }
191
+ return acc;
192
+ }, []);
193
+ }
194
+
195
+ return results;
196
+ },
197
+ [allComponents, dataset.hash, getLastViewed, makeFuse]
198
+ );
199
+ const deferredQuery = useDeferredValue(inputValue);
200
+ const input = deferredQuery ? deferredQuery.trim() : '';
201
+ const results = input ? getResults(input) : [];
202
+
203
+ return (
204
+ <View style={{ flex: 1 }}>
205
+ <SearchField>
206
+ <SearchIconWrapper>
207
+ <SearchIcon />
208
+ </SearchIconWrapper>
209
+
210
+ <Input ref={inputRef} onChangeText={setInputValue} onFocus={() => setIsOpen(true)} />
211
+
212
+ {isOpen && (
213
+ <ClearIcon
214
+ onPress={() => {
215
+ setInputValue('');
216
+ inputRef.current.clear();
217
+ }}
218
+ >
219
+ <CloseIcon />
220
+ </ClearIcon>
221
+ )}
222
+ </SearchField>
223
+
224
+ {children({
225
+ query: input,
226
+ results,
227
+ isBrowsing: !isOpen || !inputValue.length,
228
+ closeMenu: () => {},
229
+ getItemProps,
230
+ highlightedIndex: null,
231
+ })}
232
+ </View>
233
+ );
234
+ });
@@ -0,0 +1,102 @@
1
+ import type { StoryObj, Meta } from '@storybook/react';
2
+ import { SearchResults } from './SearchResults';
3
+
4
+ const meta = {
5
+ component: SearchResults,
6
+ title: 'UI/SearchResults',
7
+ } satisfies Meta<typeof SearchResults>;
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = {
14
+ args: {
15
+ query: 'bubble',
16
+ closeMenu: () => {},
17
+ highlightedIndex: null,
18
+ results: [
19
+ {
20
+ item: {
21
+ type: 'story',
22
+ id: 'nestingexample-message-bubble--first',
23
+ name: 'First',
24
+ title: 'NestingExample/Message/bubble',
25
+ importPath: './components/NestingExample/ChatMessageBubble.stories.tsx',
26
+ tags: ['story'],
27
+ depth: 3,
28
+ parent: 'nestingexample-message-bubble',
29
+ prepared: false,
30
+ refId: 'storybook_internal',
31
+ path: ['NestingExample', 'Message', 'bubble'],
32
+ status: null,
33
+ },
34
+ refIndex: 46,
35
+ matches: [
36
+ {
37
+ indices: [[0, 5]],
38
+ value: 'bubble',
39
+ key: 'path',
40
+ refIndex: 2,
41
+ },
42
+ ],
43
+ score: 0.000020134092876783674,
44
+ },
45
+ ],
46
+ getItemProps: () => ({
47
+ icon: 'story',
48
+ result: {
49
+ item: {
50
+ type: 'story',
51
+ id: 'nestingexample-message-bubble--first',
52
+ name: 'First',
53
+ title: 'NestingExample/Message/bubble',
54
+ importPath: './components/NestingExample/ChatMessageBubble.stories.tsx',
55
+ tags: ['story'],
56
+ depth: 3,
57
+ parent: 'nestingexample-message-bubble',
58
+ prepared: false,
59
+ refId: 'storybook_internal',
60
+ path: ['NestingExample', 'Message', 'bubble'],
61
+ status: null,
62
+ },
63
+ refIndex: 46,
64
+ matches: [
65
+ {
66
+ indices: [[0, 5]],
67
+ value: 'bubble',
68
+ key: 'path',
69
+ refIndex: 2,
70
+ },
71
+ ],
72
+ score: 0.000020134092876783674,
73
+ },
74
+ score: 0.000020134092876783674,
75
+ refIndex: 46,
76
+ item: {
77
+ type: 'story',
78
+ id: 'nestingexample-message-bubble--first',
79
+ name: 'First',
80
+ title: 'NestingExample/Message/bubble',
81
+ importPath: './components/NestingExample/ChatMessageBubble.stories.tsx',
82
+ tags: ['story'],
83
+ depth: 3,
84
+ parent: 'nestingexample-message-bubble',
85
+ prepared: false,
86
+ refId: 'storybook_internal',
87
+ path: ['NestingExample', 'Message', 'bubble'],
88
+ status: null,
89
+ },
90
+ matches: [
91
+ {
92
+ indices: [[0, 5]],
93
+ value: 'bubble',
94
+ key: 'path',
95
+ refIndex: 2,
96
+ },
97
+ ],
98
+ isHighlighted: false,
99
+ onPress: () => {},
100
+ }),
101
+ },
102
+ };
@@ -0,0 +1,254 @@
1
+ import { styled } from '@storybook/react-native-theming';
2
+ import type { FC, PropsWithChildren, ReactNode } from 'react';
3
+ import React, { useCallback } from 'react';
4
+ import { transparentize } from 'polished';
5
+ import type { GetSearchItemProps, SearchResult, SearchResultProps } from './types';
6
+ import { isExpandType } from './types';
7
+
8
+ import { FuseResultMatch } from 'fuse.js';
9
+ import { PressableProps, View } from 'react-native';
10
+ import { Button } from './Button';
11
+ import { IconButton } from './IconButton';
12
+
13
+ import { statusMapping } from './util/status';
14
+ import { ComponentIcon, StoryIcon } from './icon/iconDataUris';
15
+
16
+ const ResultsList = styled.View({
17
+ margin: 0,
18
+ padding: 0,
19
+ marginTop: 8,
20
+ });
21
+
22
+ const ResultRow = styled.TouchableOpacity<{ isHighlighted: boolean }>(
23
+ ({ theme, isHighlighted }) => ({
24
+ width: '100%',
25
+ border: 'none',
26
+ cursor: 'pointer',
27
+ display: 'flex',
28
+ flexDirection: 'row',
29
+ alignItems: 'flex-start',
30
+ textAlign: 'left',
31
+ color: theme.color.defaultText,
32
+ fontSize: theme.typography.size.s2,
33
+ backgroundColor: isHighlighted ? theme.background.hoverable : 'transparent',
34
+ minHeight: 28,
35
+ borderRadius: 4,
36
+ gap: 6,
37
+ paddingTop: 7,
38
+ paddingBottom: 7,
39
+ paddingLeft: 8,
40
+ paddingRight: 8,
41
+
42
+ '&:hover, &:focus': {
43
+ backgroundColor: transparentize(0.93, theme.color.secondary),
44
+ outline: 'none',
45
+ },
46
+ })
47
+ );
48
+
49
+ const IconWrapper = styled.View({
50
+ marginTop: 2,
51
+ });
52
+
53
+ const ResultRowContent = styled.View(() => ({
54
+ display: 'flex',
55
+ flexDirection: 'column',
56
+ }));
57
+
58
+ const NoResults = styled.View(({ theme }) => ({
59
+ marginTop: 20,
60
+ textAlign: 'center',
61
+ fontSize: theme.typography.size.s2,
62
+ lineHeight: 18,
63
+ color: theme.color.defaultText,
64
+ }));
65
+
66
+ const Mark = styled.Text(({ theme }) => ({
67
+ backgroundColor: 'transparent',
68
+ color: theme.color.secondary,
69
+ }));
70
+
71
+ const MoreWrapper = styled.View({
72
+ marginTop: 8,
73
+ });
74
+
75
+ const RecentlyOpenedTitle = styled.View(({ theme }) => ({
76
+ display: 'flex',
77
+ flexDirection: 'row',
78
+ justifyContent: 'space-between',
79
+ fontSize: theme.typography.size.s1 - 1,
80
+ fontWeight: theme.typography.weight.bold,
81
+ minHeight: 28,
82
+ // letterSpacing: '0.16em', <-- todo
83
+ textTransform: 'uppercase',
84
+ color: theme.textMutedColor,
85
+ marginTop: 16,
86
+ marginBottom: 4,
87
+ alignItems: 'center',
88
+ }));
89
+
90
+ const Highlight: FC<PropsWithChildren<{ match?: FuseResultMatch }>> = React.memo(
91
+ function Highlight({ children, match }) {
92
+ if (!match) return children;
93
+ const { value, indices } = match;
94
+
95
+ const { nodes: result } = indices.reduce<{ cursor: number; nodes: ReactNode[] }>(
96
+ ({ cursor, nodes }, [start, end], index, { length }) => {
97
+ nodes.push(<Text key={`text-${index}`}>{value.slice(cursor, start)}</Text>);
98
+ nodes.push(<Mark key={`mark-${index}`}>{value.slice(start, end + 1)}</Mark>);
99
+ if (index === length - 1) {
100
+ nodes.push(<Text key={`last-${index}`}>{value.slice(end + 1)}</Text>);
101
+ }
102
+ return { cursor: end + 1, nodes };
103
+ },
104
+ { cursor: 0, nodes: [] }
105
+ );
106
+ return <Text key={`end-${match.key}`}>{result}</Text>;
107
+ }
108
+ );
109
+
110
+ const Title = styled.Text(({ theme }) => ({
111
+ justifyContent: 'flex-start',
112
+ color: theme.textMutedColor,
113
+ fontSize: theme.typography.size.s2,
114
+ }));
115
+
116
+ const Path = styled.View(({ theme }) => ({
117
+ justifyContent: 'flex-start',
118
+ marginVertical: 2,
119
+ color: theme.textMutedColor,
120
+ fontSize: theme.typography.size.s1 - 1,
121
+ flexDirection: 'row',
122
+ }));
123
+
124
+ const PathText = styled.Text(({ theme }) => ({
125
+ fontSize: theme.typography.size.s1 - 1,
126
+ color: theme.textMutedColor,
127
+ }));
128
+
129
+ const Result: FC<SearchResultProps> = React.memo(function Result({
130
+ item,
131
+ matches,
132
+ icon: _icon,
133
+ onPress,
134
+ ...props
135
+ }) {
136
+ const press: PressableProps['onPress'] = useCallback(
137
+ (event) => {
138
+ event.preventDefault();
139
+ onPress?.(event);
140
+ },
141
+ [onPress]
142
+ );
143
+
144
+ const nameMatch = matches.find((match: FuseResultMatch) => match.key === 'name');
145
+ const pathMatches = matches.filter((match: FuseResultMatch) => match.key === 'path');
146
+
147
+ const [i] = item.status ? statusMapping[item.status] : [];
148
+
149
+ return (
150
+ <ResultRow {...props} onPress={press}>
151
+ <IconWrapper>
152
+ {item.type === 'component' && <ComponentIcon width={14} height={14} />}
153
+ {item.type === 'story' && <StoryIcon width={14} height={14} />}
154
+ </IconWrapper>
155
+ <ResultRowContent testID="search-result-item--label">
156
+ <Title>
157
+ <Highlight key="search-result-item--label-highlight" match={nameMatch}>
158
+ {item.name}
159
+ </Highlight>
160
+ </Title>
161
+ <Path>
162
+ {item.path.map((group, index) => {
163
+ const pathSeparator = index === item.path.length - 1 ? '' : '/';
164
+ return (
165
+ <View key={index} style={{ flexShrink: 1 }}>
166
+ <PathText>
167
+ <Highlight
168
+ match={pathMatches.find((match: FuseResultMatch) => match.refIndex === index)}
169
+ >
170
+ {`${group}${pathSeparator}`}
171
+ </Highlight>
172
+ </PathText>
173
+ </View>
174
+ );
175
+ })}
176
+ </Path>
177
+ </ResultRowContent>
178
+ {item.status ? i : null}
179
+ </ResultRow>
180
+ );
181
+ });
182
+
183
+ const Text = styled.Text(({ theme }) => ({
184
+ color: theme.color.defaultText,
185
+ }));
186
+
187
+ export const SearchResults: FC<{
188
+ query: string;
189
+ results: SearchResult[];
190
+ closeMenu: (cb?: () => void) => void;
191
+ getItemProps: GetSearchItemProps;
192
+ highlightedIndex: number | null;
193
+ isLoading?: boolean;
194
+ enableShortcuts?: boolean;
195
+ clearLastViewed?: () => void;
196
+ }> = React.memo(function SearchResults({
197
+ query,
198
+ results,
199
+ closeMenu,
200
+ getItemProps,
201
+ highlightedIndex,
202
+ clearLastViewed,
203
+ }) {
204
+ const handleClearLastViewed = () => {
205
+ clearLastViewed();
206
+ closeMenu();
207
+ };
208
+
209
+ return (
210
+ <ResultsList>
211
+ {results.length > 0 && !query ? (
212
+ <RecentlyOpenedTitle>
213
+ <Text>Recently opened</Text>
214
+ <IconButton onPress={handleClearLastViewed} />
215
+ </RecentlyOpenedTitle>
216
+ ) : null}
217
+
218
+ {results.length === 0 && query ? (
219
+ <View>
220
+ <NoResults>
221
+ <Text style={{ marginBottom: 8 }}>No components found</Text>
222
+ <Text>Find components by name or path.</Text>
223
+ </NoResults>
224
+ </View>
225
+ ) : null}
226
+
227
+ {results.map((result, index) => {
228
+ if (isExpandType(result)) {
229
+ return (
230
+ <MoreWrapper key="search-result-expand">
231
+ <Button
232
+ {...result}
233
+ {...getItemProps({ key: `${index}`, index, item: result })}
234
+ size="small"
235
+ text={`Show ${result.moreCount} more results`}
236
+ />
237
+ </MoreWrapper>
238
+ );
239
+ }
240
+
241
+ const { item } = result;
242
+ const key = `${item.refId}::${item.id}`;
243
+ return (
244
+ <Result
245
+ {...result}
246
+ {...getItemProps({ key, index, item: result })}
247
+ isHighlighted={highlightedIndex === index}
248
+ key={item.id}
249
+ />
250
+ );
251
+ })}
252
+ </ResultsList>
253
+ );
254
+ });
@@ -0,0 +1,58 @@
1
+ import type { FC, PropsWithChildren } from 'react';
2
+ import { createContext, useCallback, useContext, useRef } from 'react';
3
+ import type { ScrollView, View } from 'react-native';
4
+
5
+ type SelectedNodeContextType = {
6
+ nodeRef: React.RefObject<View>;
7
+ setNodeRef: (node: View | null) => void;
8
+ scrollToSelectedNode: () => void;
9
+ scrollRef: React.RefObject<ScrollView>;
10
+ };
11
+
12
+ const SelectedNodeContext = createContext<SelectedNodeContextType>({
13
+ nodeRef: { current: null },
14
+ setNodeRef: () => {},
15
+ scrollToSelectedNode: () => {},
16
+ scrollRef: null,
17
+ });
18
+
19
+ export const SelectedNodeProvider: FC<PropsWithChildren> = ({ children }) => {
20
+ const nodeRef = useRef<View | null>(null);
21
+
22
+ const setNodeRef = useCallback((node: View | null) => {
23
+ nodeRef.current = node;
24
+ }, []);
25
+
26
+ const scrollRef = useRef<ScrollView>(null);
27
+
28
+ const scrollToSelectedNode = useCallback(() => {
29
+ // maybe later we can improve on this to not use setTimeout but right now it seems like the simplest solution
30
+ setTimeout(() => {
31
+ if (nodeRef?.current && scrollRef?.current) {
32
+ // im just not sure if older versions would error here,
33
+ // since measure layout probably changed since new arch
34
+ try {
35
+ nodeRef.current.measureLayout?.(scrollRef.current as any, (_x, y) => {
36
+ scrollRef.current?.scrollTo({ y: y - 100, animated: true });
37
+ });
38
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
39
+ } catch (_error) {}
40
+ }
41
+ }, 500);
42
+ }, []);
43
+
44
+ return (
45
+ <SelectedNodeContext.Provider
46
+ value={{
47
+ nodeRef,
48
+ setNodeRef,
49
+ scrollToSelectedNode,
50
+ scrollRef,
51
+ }}
52
+ >
53
+ {children}
54
+ </SelectedNodeContext.Provider>
55
+ );
56
+ };
57
+
58
+ export const useSelectedNode = () => useContext(SelectedNodeContext);