@storybook/react-native-ui-common 9.0.0-beta.15
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/LICENSE +21 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.js +812 -0
- package/package.json +81 -0
- package/src/Button.stories.tsx +133 -0
- package/src/Button.tsx +171 -0
- package/src/IconButton.tsx +10 -0
- package/src/LayoutProvider.tsx +32 -0
- package/src/StorageProvider.tsx +21 -0
- package/src/assets/react-native-logo.png +0 -0
- package/src/constants.ts +4 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useExpanded.ts +64 -0
- package/src/hooks/useLastViewed.ts +35 -0
- package/src/hooks/useStoreState.ts +27 -0
- package/src/index.tsx +7 -0
- package/src/types.ts +66 -0
- package/src/util/StoryHash.ts +365 -0
- package/src/util/index.ts +3 -0
- package/src/util/tree.ts +93 -0
- package/src/util/useStyle.ts +28 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { sanitize } from '@storybook/csf';
|
|
2
|
+
import type { API, State } from 'storybook/internal/manager-api';
|
|
3
|
+
import type {
|
|
4
|
+
API_ComponentEntry,
|
|
5
|
+
API_DocsEntry,
|
|
6
|
+
API_GroupEntry,
|
|
7
|
+
API_HashEntry,
|
|
8
|
+
API_IndexHash,
|
|
9
|
+
API_PreparedStoryIndex,
|
|
10
|
+
API_Provider,
|
|
11
|
+
API_RootEntry,
|
|
12
|
+
API_StoryEntry,
|
|
13
|
+
DocsOptions,
|
|
14
|
+
IndexEntry,
|
|
15
|
+
StatusesByStoryIdAndTypeId,
|
|
16
|
+
StoryIndexV2,
|
|
17
|
+
StoryIndexV3,
|
|
18
|
+
Tag,
|
|
19
|
+
} from 'storybook/internal/types';
|
|
20
|
+
import { dedent } from 'ts-dedent';
|
|
21
|
+
import { logger } from 'storybook/internal/client-logger';
|
|
22
|
+
import { countBy, isEqual, mergeWith } from 'es-toolkit';
|
|
23
|
+
|
|
24
|
+
type ToStoriesHashOptions = {
|
|
25
|
+
provider: API_Provider<API>;
|
|
26
|
+
docsOptions: DocsOptions;
|
|
27
|
+
filters: State['filters'];
|
|
28
|
+
allStatuses: StatusesByStoryIdAndTypeId;
|
|
29
|
+
};
|
|
30
|
+
export const intersect = <T>(a: T[], b: T[]): T[] => {
|
|
31
|
+
// no point in intersecting if one of the input is ill-defined
|
|
32
|
+
if (!Array.isArray(a) || !Array.isArray(b) || !a.length || !b.length) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return a.reduce((acc: T[], aValue) => {
|
|
37
|
+
if (b.includes(aValue)) {
|
|
38
|
+
acc.push(aValue);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return acc;
|
|
42
|
+
}, []);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const merge = <TObj = any>(a: TObj, ...b: Partial<TObj>[]): TObj => {
|
|
46
|
+
// start with empty object
|
|
47
|
+
let target = {};
|
|
48
|
+
|
|
49
|
+
// merge object a unto target
|
|
50
|
+
target = mergeWith(
|
|
51
|
+
{},
|
|
52
|
+
a as Record<PropertyKey, any>,
|
|
53
|
+
(objValue: TObj, srcValue: Partial<TObj>) => {
|
|
54
|
+
if (Array.isArray(srcValue) && Array.isArray(objValue)) {
|
|
55
|
+
srcValue.forEach((s) => {
|
|
56
|
+
const existing = objValue.find((o) => o === s || isEqual(o, s));
|
|
57
|
+
if (!existing) {
|
|
58
|
+
objValue.push(s);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return objValue;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(objValue)) {
|
|
65
|
+
logger.log(['the types mismatch, picking', objValue]);
|
|
66
|
+
return objValue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const obj of b) {
|
|
72
|
+
// merge object b unto target
|
|
73
|
+
target = mergeWith(target, obj, (objValue: TObj, srcValue: Partial<TObj>) => {
|
|
74
|
+
if (Array.isArray(srcValue) && Array.isArray(objValue)) {
|
|
75
|
+
srcValue.forEach((s) => {
|
|
76
|
+
const existing = objValue.find((o) => o === s || isEqual(o, s));
|
|
77
|
+
if (!existing) {
|
|
78
|
+
objValue.push(s);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return objValue;
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(objValue)) {
|
|
85
|
+
logger.log(['the types mismatch, picking', objValue]);
|
|
86
|
+
return objValue;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return target as TObj;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const noArrayMerge = <TObj = any>(a: TObj, ...b: Partial<TObj>[]): TObj => {
|
|
95
|
+
// start with empty object
|
|
96
|
+
let target = {};
|
|
97
|
+
|
|
98
|
+
// merge object a unto target
|
|
99
|
+
target = mergeWith(
|
|
100
|
+
{},
|
|
101
|
+
a as Record<PropertyKey, any>,
|
|
102
|
+
(objValue: TObj, srcValue: Partial<TObj>) => {
|
|
103
|
+
// Treat arrays as scalars:
|
|
104
|
+
if (Array.isArray(srcValue)) {
|
|
105
|
+
return srcValue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
for (const obj of b) {
|
|
111
|
+
// merge object b unto target
|
|
112
|
+
target = mergeWith(target, obj, (objValue: TObj, srcValue: Partial<TObj>) => {
|
|
113
|
+
// Treat arrays as scalars:
|
|
114
|
+
if (Array.isArray(srcValue)) {
|
|
115
|
+
return srcValue;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return target as TObj;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const TITLE_PATH_SEPARATOR = /\s*\/\s*/;
|
|
124
|
+
|
|
125
|
+
export const transformStoryIndexToStoriesHash = (
|
|
126
|
+
input: API_PreparedStoryIndex | StoryIndexV2 | StoryIndexV3,
|
|
127
|
+
{ provider, docsOptions, filters, allStatuses }: ToStoriesHashOptions
|
|
128
|
+
): API_IndexHash | any => {
|
|
129
|
+
if (!input.v) {
|
|
130
|
+
throw new Error('Composition: Missing stories.json version');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let index = input;
|
|
134
|
+
index = index.v === 2 ? transformStoryIndexV2toV3(index as any) : index;
|
|
135
|
+
index = index.v === 3 ? transformStoryIndexV3toV4(index as any) : index;
|
|
136
|
+
index = index.v === 4 ? transformStoryIndexV4toV5(index as any) : index;
|
|
137
|
+
index = index as API_PreparedStoryIndex;
|
|
138
|
+
|
|
139
|
+
const entryValues = Object.values(index.entries).filter((entry: any) => {
|
|
140
|
+
let result = true;
|
|
141
|
+
|
|
142
|
+
// All stories with a failing status should always show up, regardless of the applied filters
|
|
143
|
+
const storyStatuses = allStatuses[entry.id] ?? {};
|
|
144
|
+
if (Object.values(storyStatuses).some(({ value }) => value === 'status-value:error')) {
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Object.values(filters).forEach((filter) => {
|
|
149
|
+
if (result === false) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
result = filter({ ...entry, statuses: storyStatuses });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const { sidebar = {} } = provider.getConfig();
|
|
159
|
+
const { showRoots, collapsedRoots = [], renderLabel }: any = sidebar;
|
|
160
|
+
|
|
161
|
+
const setShowRoots = typeof showRoots !== 'undefined';
|
|
162
|
+
|
|
163
|
+
const storiesHashOutOfOrder = entryValues.reduce((acc: any, item: any) => {
|
|
164
|
+
if (docsOptions.docsMode && item.type !== 'docs') {
|
|
165
|
+
return acc;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// First, split the title into a set of names, separated by '/' and trimmed.
|
|
169
|
+
const { title } = item;
|
|
170
|
+
const groups = title.trim().split(TITLE_PATH_SEPARATOR);
|
|
171
|
+
const root = (!setShowRoots || showRoots) && groups.length > 1 ? [groups.shift()] : [];
|
|
172
|
+
const names = [...root, ...groups];
|
|
173
|
+
|
|
174
|
+
// Now create a "path" or sub id for each name
|
|
175
|
+
const paths = names.reduce((list, name, idx) => {
|
|
176
|
+
const parent = idx > 0 && list[idx - 1];
|
|
177
|
+
const id = sanitize(parent ? `${parent}-${name}` : name!);
|
|
178
|
+
|
|
179
|
+
if (name.trim() === '') {
|
|
180
|
+
throw new Error(dedent`Invalid title ${title} ending in slash.`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (parent === id) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
dedent`
|
|
186
|
+
Invalid part '${name}', leading to id === parentId ('${id}'), inside title '${title}'
|
|
187
|
+
|
|
188
|
+
Did you create a path that uses the separator char accidentally, such as 'Vue <docs/>' where '/' is a separator char? See https://github.com/storybookjs/storybook/issues/6128
|
|
189
|
+
`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
list.push(id);
|
|
193
|
+
return list;
|
|
194
|
+
}, [] as string[]);
|
|
195
|
+
|
|
196
|
+
// Now, let's add an entry to the hash for each path/name pair
|
|
197
|
+
paths.forEach((id: any, idx: any) => {
|
|
198
|
+
// The child is the next path, OR the story/docs entry itself
|
|
199
|
+
const childId = paths[idx + 1] || item.id;
|
|
200
|
+
|
|
201
|
+
if (root.length && idx === 0) {
|
|
202
|
+
acc[id] = merge<API_RootEntry>((acc[id] || {}) as API_RootEntry, {
|
|
203
|
+
type: 'root',
|
|
204
|
+
id,
|
|
205
|
+
name: names[idx],
|
|
206
|
+
tags: [],
|
|
207
|
+
depth: idx,
|
|
208
|
+
renderLabel,
|
|
209
|
+
startCollapsed: collapsedRoots.includes(id),
|
|
210
|
+
// Note that this will later get appended to the previous list of children (see below)
|
|
211
|
+
children: [childId],
|
|
212
|
+
});
|
|
213
|
+
// Usually the last path/name pair will be displayed as a component,
|
|
214
|
+
// *unless* there are other stories that are more deeply nested under it
|
|
215
|
+
//
|
|
216
|
+
// For example, if we had stories for both
|
|
217
|
+
// - Atoms / Button
|
|
218
|
+
// - Atoms / Button / LabelledButton
|
|
219
|
+
//
|
|
220
|
+
// In this example the entry for 'atoms-button' would *not* be a component.
|
|
221
|
+
} else if ((!acc[id] || acc[id].type === 'component') && idx === paths.length - 1) {
|
|
222
|
+
acc[id] = merge<API_ComponentEntry>((acc[id] || {}) as API_ComponentEntry, {
|
|
223
|
+
type: 'component',
|
|
224
|
+
id,
|
|
225
|
+
name: names[idx],
|
|
226
|
+
tags: [],
|
|
227
|
+
parent: paths[idx - 1],
|
|
228
|
+
depth: idx,
|
|
229
|
+
renderLabel,
|
|
230
|
+
...(childId && {
|
|
231
|
+
children: [childId],
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
acc[id] = merge<API_GroupEntry>((acc[id] || {}) as API_GroupEntry, {
|
|
236
|
+
type: 'group',
|
|
237
|
+
id,
|
|
238
|
+
name: names[idx],
|
|
239
|
+
tags: [],
|
|
240
|
+
parent: paths[idx - 1],
|
|
241
|
+
depth: idx,
|
|
242
|
+
renderLabel,
|
|
243
|
+
...(childId && {
|
|
244
|
+
children: [childId],
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Finally add an entry for the docs/story itself
|
|
251
|
+
acc[item.id] = {
|
|
252
|
+
type: 'story',
|
|
253
|
+
tags: [],
|
|
254
|
+
...item,
|
|
255
|
+
depth: paths.length,
|
|
256
|
+
parent: paths[paths.length - 1],
|
|
257
|
+
renderLabel,
|
|
258
|
+
prepared: !!item.parameters,
|
|
259
|
+
} as API_DocsEntry | API_StoryEntry;
|
|
260
|
+
|
|
261
|
+
return acc;
|
|
262
|
+
}, {} as API_IndexHash);
|
|
263
|
+
|
|
264
|
+
// This function adds a "root" or "orphan" and all of its descendents to the hash.
|
|
265
|
+
function addItem(acc: API_IndexHash | any, item: API_HashEntry | any) {
|
|
266
|
+
// If we were already inserted as part of a group, that's great.
|
|
267
|
+
if (acc[item.id]) {
|
|
268
|
+
return acc;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
acc[item.id] = item;
|
|
272
|
+
// Ensure we add the children depth-first *before* inserting any other entries,
|
|
273
|
+
// and compute tags from the children put in the accumulator afterwards, once
|
|
274
|
+
// they're all known and we can compute a sound intersection.
|
|
275
|
+
if (item.type === 'root' || item.type === 'group' || item.type === 'component') {
|
|
276
|
+
item.children.forEach((childId: any) => addItem(acc, storiesHashOutOfOrder[childId]));
|
|
277
|
+
|
|
278
|
+
item.tags = item.children.reduce((currentTags: Tag[] | null, childId: any): Tag[] => {
|
|
279
|
+
const child = acc[childId];
|
|
280
|
+
|
|
281
|
+
// On the first child, we have nothing to intersect against so we use it as a source of data.
|
|
282
|
+
return currentTags === null ? child.tags : intersect(currentTags, child.tags);
|
|
283
|
+
}, null);
|
|
284
|
+
}
|
|
285
|
+
return acc;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// We'll do two passes over the data, adding all the orphans, then all the roots
|
|
289
|
+
const orphanHash = Object.values(storiesHashOutOfOrder)
|
|
290
|
+
.filter((i: any) => i.type !== 'root' && !i.parent)
|
|
291
|
+
.reduce(addItem, {});
|
|
292
|
+
|
|
293
|
+
return Object.values(storiesHashOutOfOrder)
|
|
294
|
+
.filter((i: any) => i.type === 'root')
|
|
295
|
+
.reduce(addItem, orphanHash);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const transformStoryIndexV2toV3 = (index: StoryIndexV2): StoryIndexV3 => {
|
|
299
|
+
return {
|
|
300
|
+
v: 3,
|
|
301
|
+
stories: Object.values(index.stories).reduce(
|
|
302
|
+
(acc, entry) => {
|
|
303
|
+
acc[entry.id] = {
|
|
304
|
+
...entry,
|
|
305
|
+
title: entry.kind,
|
|
306
|
+
name: entry.name || entry.story,
|
|
307
|
+
importPath: entry.parameters.fileName || '',
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return acc;
|
|
311
|
+
},
|
|
312
|
+
{} as StoryIndexV3['stories']
|
|
313
|
+
),
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const transformStoryIndexV3toV4 = (index: StoryIndexV3): API_PreparedStoryIndex => {
|
|
318
|
+
const countByTitle = countBy(Object.values(index.stories), (item) => item.title);
|
|
319
|
+
return {
|
|
320
|
+
v: 4,
|
|
321
|
+
entries: Object.values(index.stories).reduce(
|
|
322
|
+
(acc, entry: any) => {
|
|
323
|
+
let type: IndexEntry['type'] = 'story';
|
|
324
|
+
if (
|
|
325
|
+
entry.parameters?.docsOnly ||
|
|
326
|
+
(entry.name === 'Page' && countByTitle[entry.title] === 1)
|
|
327
|
+
) {
|
|
328
|
+
type = 'docs';
|
|
329
|
+
}
|
|
330
|
+
acc[entry.id] = {
|
|
331
|
+
type,
|
|
332
|
+
...(type === 'docs' && { tags: ['stories-mdx'], storiesImports: [] }),
|
|
333
|
+
...entry,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// @ts-expect-error (we're removing something that should not be there)
|
|
337
|
+
delete acc[entry.id].story;
|
|
338
|
+
// @ts-expect-error (we're removing something that should not be there)
|
|
339
|
+
delete acc[entry.id].kind;
|
|
340
|
+
|
|
341
|
+
return acc;
|
|
342
|
+
},
|
|
343
|
+
{} as API_PreparedStoryIndex['entries']
|
|
344
|
+
),
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const transformStoryIndexV4toV5 = (
|
|
349
|
+
index: API_PreparedStoryIndex
|
|
350
|
+
): API_PreparedStoryIndex => {
|
|
351
|
+
return {
|
|
352
|
+
v: 5,
|
|
353
|
+
entries: Object.values(index.entries).reduce(
|
|
354
|
+
(acc, entry) => {
|
|
355
|
+
acc[entry.id] = {
|
|
356
|
+
...entry,
|
|
357
|
+
tags: entry.tags ? ['dev', 'test', ...entry.tags] : ['dev'],
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return acc;
|
|
361
|
+
},
|
|
362
|
+
{} as API_PreparedStoryIndex['entries']
|
|
363
|
+
),
|
|
364
|
+
};
|
|
365
|
+
};
|
package/src/util/tree.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import memoize from 'memoizerific';
|
|
2
|
+
import type { SyntheticEvent } from 'react';
|
|
3
|
+
import type { IndexHash } from 'storybook/internal/manager-api';
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_REF_ID } from '../constants';
|
|
6
|
+
import type { Item, RefType, Dataset, SearchItem } from '../types';
|
|
7
|
+
|
|
8
|
+
export const createId = (itemId: string, refId?: string) =>
|
|
9
|
+
!refId || refId === DEFAULT_REF_ID ? itemId : `${refId}_${itemId}`;
|
|
10
|
+
|
|
11
|
+
export const prevent = (e: SyntheticEvent) => {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
return false;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const get = memoize(1000)((id: string, dataset: Dataset) => dataset[id]);
|
|
17
|
+
|
|
18
|
+
export const getParent = memoize(1000)((id: string, dataset: Dataset) => {
|
|
19
|
+
const item = get(id, dataset);
|
|
20
|
+
return item && item.type !== 'root' ? get(item.parent, dataset) : undefined;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const getParents = memoize(1000)((id: string, dataset: Dataset): Item[] => {
|
|
24
|
+
const parent = getParent(id, dataset);
|
|
25
|
+
return parent ? [parent, ...getParents(parent.id, dataset)] : [];
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const getAncestorIds = memoize(1000)((data: IndexHash, id: string): string[] =>
|
|
29
|
+
getParents(id, data).map((item) => item.id)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export const getDescendantIds = memoize(1000)((
|
|
33
|
+
data: IndexHash,
|
|
34
|
+
id: string,
|
|
35
|
+
skipLeafs: boolean
|
|
36
|
+
): string[] => {
|
|
37
|
+
const entry = data[id];
|
|
38
|
+
const children = entry.type === 'story' || entry.type === 'docs' ? [] : entry.children;
|
|
39
|
+
return children.reduce((acc, childId) => {
|
|
40
|
+
const child = data[childId];
|
|
41
|
+
if (!child || (skipLeafs && (child.type === 'story' || child.type === 'docs'))) return acc;
|
|
42
|
+
acc.push(childId, ...getDescendantIds(data, childId, skipLeafs));
|
|
43
|
+
return acc;
|
|
44
|
+
}, []);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export function getPath(item: Item, ref: RefType): string[] {
|
|
48
|
+
const parent = item.type !== 'root' && item.parent ? ref.index[item.parent] : null;
|
|
49
|
+
if (parent) return [...getPath(parent, ref), parent.name];
|
|
50
|
+
return ref.id === DEFAULT_REF_ID ? [] : [ref.title || ref.id];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const searchItem = (item: Item, ref: RefType): SearchItem => {
|
|
54
|
+
return { ...item, refId: ref.id, path: getPath(item, ref) };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function cycle<T>(array: T[], index: number, delta: number): number {
|
|
58
|
+
let next = index + (delta % array.length);
|
|
59
|
+
if (next < 0) next = array.length + next;
|
|
60
|
+
if (next >= array.length) next -= array.length;
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const getStateType = (
|
|
65
|
+
isLoading: boolean,
|
|
66
|
+
isAuthRequired: boolean,
|
|
67
|
+
isError: boolean,
|
|
68
|
+
isEmpty: boolean
|
|
69
|
+
) => {
|
|
70
|
+
switch (true) {
|
|
71
|
+
case isAuthRequired:
|
|
72
|
+
return 'auth';
|
|
73
|
+
case isError:
|
|
74
|
+
return 'error';
|
|
75
|
+
case isLoading:
|
|
76
|
+
return 'loading';
|
|
77
|
+
case isEmpty:
|
|
78
|
+
return 'empty';
|
|
79
|
+
default:
|
|
80
|
+
return 'ready';
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const isAncestor = (element?: Element, maybeAncestor?: Element): boolean => {
|
|
85
|
+
if (!element || !maybeAncestor) return false;
|
|
86
|
+
if (element === maybeAncestor) return true;
|
|
87
|
+
return isAncestor(element.parentElement, maybeAncestor);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const removeNoiseFromName = (storyName: string) => storyName.replaceAll(/(\s|-|_)/gi, '');
|
|
91
|
+
|
|
92
|
+
export const isStoryHoistable = (storyName: string, componentName: string) =>
|
|
93
|
+
removeNoiseFromName(storyName) === removeNoiseFromName(componentName);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DependencyList, useMemo } from 'react';
|
|
2
|
+
import { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A hook to memoize a style. Uses `ViewStyle` per default, but can be used with other styles deriving from `FlexStyle` as well, such as `TextStyle`.
|
|
6
|
+
* @param styleFactory The function that returns a style
|
|
7
|
+
* @param deps The dependencies to trigger memoization re-evaluation
|
|
8
|
+
* @see ["Memoize!!! 💾 - a react (native) performance guide"](https://gist.github.com/mrousavy/0de7486814c655de8a110df5cef74ddc)
|
|
9
|
+
* @example
|
|
10
|
+
*
|
|
11
|
+
* // simple object styles
|
|
12
|
+
* const style1 = useStyle(() => ({ height: someDynamicValue }), [someDynamicValue])
|
|
13
|
+
*
|
|
14
|
+
* // array styles
|
|
15
|
+
* const style2 = useStyle(
|
|
16
|
+
* () => [styles.container, props.style, { height: someDynamicValue }],
|
|
17
|
+
* [props.style, someDynamicValue]
|
|
18
|
+
* );
|
|
19
|
+
*/
|
|
20
|
+
export const useStyle = <
|
|
21
|
+
TStyle extends ViewStyle | TextStyle | ImageStyle,
|
|
22
|
+
TOutput extends StyleProp<TStyle>,
|
|
23
|
+
>(
|
|
24
|
+
styleFactory: () => TOutput,
|
|
25
|
+
deps?: DependencyList
|
|
26
|
+
): TOutput =>
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
|
+
useMemo(styleFactory, deps);
|