@oxyhq/bloom 0.52.0 → 0.53.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/lib/commonjs/bottom-sheet/BottomSheetBase.js +7 -4
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/commonjs/design-tokens/shadows.js +4 -0
- package/lib/commonjs/design-tokens/shadows.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +26 -10
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/index.js +66 -66
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +90 -90
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/loading/Loading.web.js.map +1 -1
- package/lib/commonjs/loading/SpinnerIcon.web.js +10 -6
- package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/commonjs/motion/index.js +1 -1
- package/lib/commonjs/motion/index.web.js +32 -0
- package/lib/commonjs/motion/index.web.js.map +1 -0
- package/lib/commonjs/motion/motion.js +10 -2
- package/lib/commonjs/motion/motion.js.map +1 -1
- package/lib/commonjs/motion/motion.web.js +137 -0
- package/lib/commonjs/motion/motion.web.js.map +1 -0
- package/lib/commonjs/progressive-blur/index.web.js.map +1 -1
- package/lib/commonjs/styles/web-view-style.js +33 -0
- package/lib/commonjs/styles/web-view-style.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +18 -4
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/tab-bar/index.js +5 -5
- package/lib/commonjs/tab-bar/index.js.map +1 -1
- package/lib/commonjs/tab-bar/index.web.js +5 -5
- package/lib/commonjs/tab-bar/index.web.js.map +1 -1
- package/lib/commonjs/tab-bar/shared.js +3 -3
- package/lib/commonjs/tab-bar/surface.web.js.map +1 -1
- package/lib/commonjs/tab-bar/use-footprint.js +56 -0
- package/lib/commonjs/tab-bar/use-footprint.js.map +1 -0
- package/lib/commonjs/toast/ToastRow.js +7 -3
- package/lib/commonjs/toast/ToastRow.js.map +1 -1
- package/lib/commonjs/toast/ToastSwipeHandler.js +29 -13
- package/lib/commonjs/toast/ToastSwipeHandler.js.map +1 -1
- package/lib/commonjs/toast/constants.js +29 -1
- package/lib/commonjs/toast/constants.js.map +1 -1
- package/lib/commonjs/toast/position-utils.js +9 -4
- package/lib/commonjs/toast/position-utils.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +12 -9
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +7 -4
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/module/design-tokens/shadows.js +4 -0
- package/lib/module/design-tokens/shadows.js.map +1 -1
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +26 -10
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/loading/Loading.web.js.map +1 -1
- package/lib/module/loading/SpinnerIcon.web.js +10 -6
- package/lib/module/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/module/motion/index.js +1 -1
- package/lib/module/motion/index.js.map +1 -1
- package/lib/module/motion/index.web.js +10 -0
- package/lib/module/motion/index.web.js.map +1 -0
- package/lib/module/motion/motion.js +10 -2
- package/lib/module/motion/motion.js.map +1 -1
- package/lib/module/motion/motion.web.js +133 -0
- package/lib/module/motion/motion.web.js.map +1 -0
- package/lib/module/progressive-blur/index.web.js.map +1 -1
- package/lib/module/styles/web-view-style.js +33 -0
- package/lib/module/styles/web-view-style.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +18 -4
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/tab-bar/index.js +1 -1
- package/lib/module/tab-bar/index.js.map +1 -1
- package/lib/module/tab-bar/index.web.js +1 -1
- package/lib/module/tab-bar/index.web.js.map +1 -1
- package/lib/module/tab-bar/shared.js +3 -3
- package/lib/module/tab-bar/surface.web.js.map +1 -1
- package/lib/module/tab-bar/use-footprint.js +52 -0
- package/lib/module/tab-bar/use-footprint.js.map +1 -0
- package/lib/module/toast/ToastRow.js +8 -4
- package/lib/module/toast/ToastRow.js.map +1 -1
- package/lib/module/toast/ToastSwipeHandler.js +29 -13
- package/lib/module/toast/ToastSwipeHandler.js.map +1 -1
- package/lib/module/toast/constants.js +28 -1
- package/lib/module/toast/constants.js.map +1 -1
- package/lib/module/toast/position-utils.js +9 -4
- package/lib/module/toast/position-utils.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +12 -9
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/shadows.d.ts +4 -0
- package/lib/typescript/commonjs/design-tokens/shadows.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/motion/index.web.d.ts +9 -0
- package/lib/typescript/commonjs/motion/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/motion/motion.d.ts +14 -6
- package/lib/typescript/commonjs/motion/motion.d.ts.map +1 -1
- package/lib/typescript/commonjs/motion/motion.web.d.ts +79 -0
- package/lib/typescript/commonjs/motion/motion.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/progressive-blur/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/web-view-style.d.ts +49 -0
- package/lib/typescript/commonjs/styles/web-view-style.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/index.d.ts +1 -1
- package/lib/typescript/commonjs/tab-bar/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/index.web.d.ts +1 -1
- package/lib/typescript/commonjs/tab-bar/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/shared.d.ts +3 -3
- package/lib/typescript/commonjs/tab-bar/surface.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts +31 -0
- package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts.map +1 -0
- package/lib/typescript/commonjs/toast/ToastRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts +12 -6
- package/lib/typescript/commonjs/toast/ToastSwipeHandler.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/constants.d.ts +26 -8
- package/lib/typescript/commonjs/toast/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/position-utils.d.ts +8 -3
- package/lib/typescript/commonjs/toast/position-utils.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/shadows.d.ts +4 -0
- package/lib/typescript/module/design-tokens/shadows.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/module/motion/index.web.d.ts +9 -0
- package/lib/typescript/module/motion/index.web.d.ts.map +1 -0
- package/lib/typescript/module/motion/motion.d.ts +14 -6
- package/lib/typescript/module/motion/motion.d.ts.map +1 -1
- package/lib/typescript/module/motion/motion.web.d.ts +79 -0
- package/lib/typescript/module/motion/motion.web.d.ts.map +1 -0
- package/lib/typescript/module/progressive-blur/index.web.d.ts.map +1 -1
- package/lib/typescript/module/styles/web-view-style.d.ts +49 -0
- package/lib/typescript/module/styles/web-view-style.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/index.d.ts +1 -1
- package/lib/typescript/module/tab-bar/index.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/index.web.d.ts +1 -1
- package/lib/typescript/module/tab-bar/index.web.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/shared.d.ts +3 -3
- package/lib/typescript/module/tab-bar/surface.web.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/use-footprint.d.ts +31 -0
- package/lib/typescript/module/tab-bar/use-footprint.d.ts.map +1 -0
- package/lib/typescript/module/toast/ToastRow.d.ts.map +1 -1
- package/lib/typescript/module/toast/ToastSwipeHandler.d.ts +12 -6
- package/lib/typescript/module/toast/ToastSwipeHandler.d.ts.map +1 -1
- package/lib/typescript/module/toast/constants.d.ts +26 -8
- package/lib/typescript/module/toast/constants.d.ts.map +1 -1
- package/lib/typescript/module/toast/position-utils.d.ts +8 -3
- package/lib/typescript/module/toast/position-utils.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__tests__/Motion.test.tsx +31 -3
- package/src/__tests__/MotionWebFork.test.ts +140 -0
- package/src/__tests__/TabBarAnimatedShorthand.test.ts +135 -0
- package/src/__tests__/TabBarFootprint.test.tsx +172 -0
- package/src/__tests__/TabBarWebFork.test.ts +1 -1
- package/src/__tests__/ToastHostWebFork.test.ts +6 -2
- package/src/__tests__/Toaster.test.tsx +62 -1
- package/src/__tests__/toast-position-utils.test.ts +23 -7
- package/src/__tests__/web-css-style.test.ts +139 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +11 -7
- package/src/connection-dots/ConnectionDots.web.tsx +4 -3
- package/src/design-tokens/shadows.ts +5 -1
- package/src/dialog/Dialog.tsx +1 -1
- package/src/dialog/Dialog.web.tsx +41 -26
- package/src/index.web.ts +2 -2
- package/src/loading/Loading.web.tsx +7 -5
- package/src/loading/SpinnerIcon.web.tsx +12 -9
- package/src/motion/index.web.ts +8 -0
- package/src/motion/motion.ts +14 -6
- package/src/motion/motion.web.ts +99 -0
- package/src/progressive-blur/index.web.tsx +4 -3
- package/src/styles/web-view-style.ts +50 -0
- package/src/tab-bar/TabBarBase.tsx +17 -12
- package/src/tab-bar/index.ts +1 -1
- package/src/tab-bar/index.web.tsx +1 -1
- package/src/tab-bar/shared.ts +3 -3
- package/src/tab-bar/surface.web.tsx +4 -3
- package/src/tab-bar/use-footprint.ts +50 -0
- package/src/toast/ToastRow.tsx +8 -3
- package/src/toast/ToastSwipeHandler.tsx +30 -12
- package/src/toast/constants.ts +28 -0
- package/src/toast/position-utils.ts +10 -5
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +9 -8
- package/lib/commonjs/tab-bar/use-reserved-space.js +0 -50
- package/lib/commonjs/tab-bar/use-reserved-space.js.map +0 -1
- package/lib/module/tab-bar/use-reserved-space.js +0 -46
- package/lib/module/tab-bar/use-reserved-space.js.map +0 -1
- package/lib/typescript/commonjs/tab-bar/use-reserved-space.d.ts +0 -25
- package/lib/typescript/commonjs/tab-bar/use-reserved-space.d.ts.map +0 -1
- package/lib/typescript/module/tab-bar/use-reserved-space.d.ts +0 -25
- package/lib/typescript/module/tab-bar/use-reserved-space.d.ts.map +0 -1
- package/src/__tests__/TabBarReservedSpace.test.tsx +0 -150
- package/src/tab-bar/use-reserved-space.ts +0 -44
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* No reanimated mapper may return a React Native style SHORTHAND that
|
|
6
|
+
* react-native-web does not expand.
|
|
7
|
+
*
|
|
8
|
+
* Reanimated's web path hands a mapper's result to RNW's `createReactDOMStyle`
|
|
9
|
+
* and then assigns each surviving key straight onto the DOM node's `style`. That
|
|
10
|
+
* function expands a fixed set of shorthands (`marginInline`, `paddingInline`,
|
|
11
|
+
* `borderWidth`, `borderRadius`, `insetInline`, …) but NOT React Native's own
|
|
12
|
+
* `marginHorizontal` / `marginVertical` / `paddingHorizontal` / `paddingVertical`
|
|
13
|
+
* — verified absent from the entire file in RNW 0.21.2. An unexpanded shorthand
|
|
14
|
+
* becomes `element.style.marginHorizontal = '34px'`, which is not a CSS property,
|
|
15
|
+
* so the browser drops it WITHOUT any error.
|
|
16
|
+
*
|
|
17
|
+
* This shipped once: `barStyle` animated `marginHorizontal` (copied verbatim from
|
|
18
|
+
* upstream expo-glass-tabs, which only ever ran on native). On web the bar kept
|
|
19
|
+
* its full width while `highlightStyle` still subtracted `MINIMIZED_INSET` from
|
|
20
|
+
* `barWidth`, so the sliding highlight sat ~13.6px narrower than the tab beneath
|
|
21
|
+
* it at five tabs — visible only once the bar minimized.
|
|
22
|
+
*
|
|
23
|
+
* STATIC styles are fine and are deliberately not scanned: `StyleSheet.create`
|
|
24
|
+
* takes RNW's own StyleSheet path, which does handle the shorthand. The trap is
|
|
25
|
+
* specific to values produced inside a mapper.
|
|
26
|
+
*
|
|
27
|
+
* Neither tsc nor jest can see this: the shorthand is a valid RN style key, so it
|
|
28
|
+
* type-checks, and the reanimated mock never touches the DOM. Source analysis is
|
|
29
|
+
* the only automatable guard.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const SRC = join(__dirname, '..');
|
|
33
|
+
const ROOTS = ['tab-bar', 'progressive-blur'] as const;
|
|
34
|
+
const MAPPERS = ['useAnimatedStyle', 'useDerivedValue', 'useAnimatedProps'] as const;
|
|
35
|
+
|
|
36
|
+
/** RN shorthands with no entry in RNW's `STYLE_SHORT_FORM_EXPANSIONS`. */
|
|
37
|
+
const UNEXPANDED_SHORTHANDS = [
|
|
38
|
+
'marginHorizontal',
|
|
39
|
+
'marginVertical',
|
|
40
|
+
'paddingHorizontal',
|
|
41
|
+
'paddingVertical',
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
function sourceFiles(root: string): string[] {
|
|
45
|
+
const out: string[] = [];
|
|
46
|
+
const walk = (dir: string) => {
|
|
47
|
+
for (const entry of readdirSync(dir)) {
|
|
48
|
+
const full = join(dir, entry);
|
|
49
|
+
if (statSync(full).isDirectory()) {
|
|
50
|
+
walk(full);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!/\.tsx?$/.test(entry)) continue;
|
|
54
|
+
if (/\.(test|spec|stories)\.tsx?$/.test(entry)) continue;
|
|
55
|
+
out.push(full);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
walk(join(SRC, root));
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The body of every mapper call in `source`, matched by brace depth from the
|
|
64
|
+
* mapper's opening paren so nested objects and callbacks are included whole.
|
|
65
|
+
*/
|
|
66
|
+
function mapperBodies(source: string): string[] {
|
|
67
|
+
const bodies: string[] = [];
|
|
68
|
+
for (const mapper of MAPPERS) {
|
|
69
|
+
let from = 0;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const start = source.indexOf(`${mapper}(`, from);
|
|
72
|
+
if (start === -1) break;
|
|
73
|
+
let depth = 0;
|
|
74
|
+
let index = source.indexOf('(', start);
|
|
75
|
+
const open = index;
|
|
76
|
+
for (; index < source.length; index += 1) {
|
|
77
|
+
const char = source[index];
|
|
78
|
+
if (char === '(') depth += 1;
|
|
79
|
+
else if (char === ')') {
|
|
80
|
+
depth -= 1;
|
|
81
|
+
if (depth === 0) break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
bodies.push(source.slice(open, index));
|
|
85
|
+
from = index === -1 ? start + mapper.length : index;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return bodies;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('tab-bar animated styles', () => {
|
|
92
|
+
const files = ROOTS.flatMap(sourceFiles);
|
|
93
|
+
|
|
94
|
+
// Self-check: a broken walk must not pass vacuously.
|
|
95
|
+
it('walks the real sources', () => {
|
|
96
|
+
expect(files.length).toBeGreaterThanOrEqual(10);
|
|
97
|
+
const withMappers = files.filter((file) => mapperBodies(readFileSync(file, 'utf8')).length > 0);
|
|
98
|
+
expect(withMappers.length).toBeGreaterThanOrEqual(2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('extracts a mapper body that really contains its returned keys', () => {
|
|
102
|
+
const base = files.find((file) => file.endsWith('TabBarBase.tsx'));
|
|
103
|
+
expect(base).toBeDefined();
|
|
104
|
+
const bodies = mapperBodies(readFileSync(base as string, 'utf8'));
|
|
105
|
+
expect(bodies.length).toBeGreaterThanOrEqual(6);
|
|
106
|
+
// `highlightStyle` is the mapper that computes the capsule geometry.
|
|
107
|
+
expect(bodies.some((body) => body.includes('borderRadius'))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it.each(UNEXPANDED_SHORTHANDS)('never returns %s from a mapper', (shorthand) => {
|
|
111
|
+
const offenders: string[] = [];
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
for (const body of mapperBodies(readFileSync(file, 'utf8'))) {
|
|
114
|
+
// Strip comments so the explanatory notes naming the shorthand do not
|
|
115
|
+
// trip the guard that exists because of them.
|
|
116
|
+
const code = body.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
|
117
|
+
if (new RegExp(`\\b${shorthand}\\s*:`).test(code)) {
|
|
118
|
+
offenders.push(relative(SRC, file));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
expect(offenders).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('animates the minimized inset as both long-form margins', () => {
|
|
126
|
+
const base = readFileSync(
|
|
127
|
+
files.find((file) => file.endsWith('TabBarBase.tsx')) as string,
|
|
128
|
+
'utf8',
|
|
129
|
+
);
|
|
130
|
+
const barStyle = mapperBodies(base).find((body) => body.includes('MINIMIZED_INSET'));
|
|
131
|
+
expect(barStyle).toBeDefined();
|
|
132
|
+
expect(barStyle).toMatch(/marginLeft:/);
|
|
133
|
+
expect(barStyle).toMatch(/marginRight:/);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ReactTestInstance } from 'react-test-renderer';
|
|
3
|
+
import { render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
6
|
+
import { TabBar, TabBarButton, useTabBarFootprint } from '../tab-bar';
|
|
7
|
+
import type { TabBarItem } from '../tab-bar/types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `useTabBarFootprint()` — the space the floating bar physically occupies above
|
|
11
|
+
* the bottom of the window, and nothing more.
|
|
12
|
+
*
|
|
13
|
+
* The bar is absolutely positioned, so nothing accounts for it automatically,
|
|
14
|
+
* and the geometry it is built from (expanded height, bottom gap) is private.
|
|
15
|
+
* Without the hook a consumer hardcodes both — which drifts silently the first
|
|
16
|
+
* time the bar changes by a pixel — and, worse, tends to add `insets.bottom` on
|
|
17
|
+
* top, because that is what laying anything out against the bottom edge
|
|
18
|
+
* normally requires. The bar already folds the inset into its own gap, so that
|
|
19
|
+
* double-counts the home indicator.
|
|
20
|
+
*
|
|
21
|
+
* It is the RAW footprint: no clearance is folded in, because clearance differs
|
|
22
|
+
* per call site (a list wants a gap above the pill, a FAB anchored by `bottom`
|
|
23
|
+
* supplies its own) and only the consumer knows which. That is asserted here
|
|
24
|
+
* rather than merely documented — a margin quietly added to the hook would move
|
|
25
|
+
* every consumer's FAB.
|
|
26
|
+
*
|
|
27
|
+
* The suite asserts four separate things: the absolute pixel values (so a change
|
|
28
|
+
* to the constants must be deliberate), that the number is the bar and only the
|
|
29
|
+
* bar, the inset relationship (so the double-count can never creep back in), and
|
|
30
|
+
* equality with the bar's own rendered geometry (so it cannot drift from where
|
|
31
|
+
* the bar actually sits).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// Mutated per test; must be `mock`-prefixed for the hoisted factory.
|
|
35
|
+
const mockInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
36
|
+
|
|
37
|
+
jest.mock('react-native-safe-area-context', () => ({
|
|
38
|
+
...jest.requireActual('react-native-safe-area-context'),
|
|
39
|
+
useSafeAreaInsets: () => mockInsets,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const ITEMS: TabBarItem[] = [
|
|
43
|
+
{ name: 'home', label: 'Home', icon: null },
|
|
44
|
+
{ name: 'search', label: 'Search', icon: null },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** A device with a home indicator (iPhone-class bottom inset). */
|
|
48
|
+
const HOME_INDICATOR_INSET = 34;
|
|
49
|
+
|
|
50
|
+
function FootprintProbe({ onValue }: { onValue: (space: number) => void }) {
|
|
51
|
+
onValue(useTabBarFootprint());
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function withTheme(ui: React.ReactElement) {
|
|
56
|
+
return (
|
|
57
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
58
|
+
{ui}
|
|
59
|
+
</BloomThemeProvider>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The footprint the hook reports on a device whose bottom inset is `inset`. */
|
|
64
|
+
function footprintFor(inset: number): number {
|
|
65
|
+
mockInsets.bottom = inset;
|
|
66
|
+
let space: number | undefined;
|
|
67
|
+
render(withTheme(<FootprintProbe onValue={(value) => (space = value)} />));
|
|
68
|
+
if (space === undefined) throw new Error('FootprintProbe never ran');
|
|
69
|
+
return space;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function flattenStyle(style: unknown): Record<string, unknown> {
|
|
73
|
+
const out: Record<string, unknown> = {};
|
|
74
|
+
const visit = (value: unknown): void => {
|
|
75
|
+
if (Array.isArray(value)) value.forEach(visit);
|
|
76
|
+
else if (value && typeof value === 'object') Object.assign(out, value);
|
|
77
|
+
};
|
|
78
|
+
visit(style);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The flattened style of every styled node in the tree.
|
|
84
|
+
*
|
|
85
|
+
* Restricted to HOST nodes: the RN mock renders each component as a composite
|
|
86
|
+
* wrapping a host element of the same name, and both carry the same `style`, so
|
|
87
|
+
* counting all of them would double every match.
|
|
88
|
+
*/
|
|
89
|
+
function styledNodes(root: ReactTestInstance): Record<string, unknown>[] {
|
|
90
|
+
return root
|
|
91
|
+
.findAll((node) => typeof node.type === 'string' && node.props?.style !== undefined)
|
|
92
|
+
.map((node) => flattenStyle(node.props.style));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('useTabBarFootprint', () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
mockInsets.bottom = 0;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('measures the bar plus its gap where there is no bottom inset', () => {
|
|
101
|
+
// 58pt bar + the 12pt floor the gap never goes below.
|
|
102
|
+
expect(footprintFor(0)).toBe(70);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('measures more on a device with a home indicator', () => {
|
|
106
|
+
expect(footprintFor(HOME_INDICATOR_INSET)).toBe(76);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('adds no clearance of its own — the number is the bar and only the bar', () => {
|
|
110
|
+
// Clearance belongs to the call site: a list wants a gap above the pill, a
|
|
111
|
+
// FAB anchored by `bottom` supplies its own. A margin folded in here would
|
|
112
|
+
// lift every consumer's FAB off its intended position, and would make
|
|
113
|
+
// "footprint" mean two different things in one file.
|
|
114
|
+
expect(footprintFor(0)).toBe(58 + 12);
|
|
115
|
+
expect(footprintFor(HOME_INDICATOR_INSET)).toBe(58 + (HOME_INDICATOR_INSET - 16));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('already includes the inset — adding insets.bottom would double-count it', () => {
|
|
119
|
+
// The bar absorbs 16pt of the inset into its own gap, so the footprint grows
|
|
120
|
+
// by the REMAINDER, never by the whole inset. A consumer writing
|
|
121
|
+
// `footprint + insets.bottom` would account 110pt for a 76pt bar.
|
|
122
|
+
const grown = footprintFor(HOME_INDICATOR_INSET) - footprintFor(0);
|
|
123
|
+
expect(grown).toBe(6);
|
|
124
|
+
expect(grown).toBeLessThan(HOME_INDICATOR_INSET);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does not shrink below the floor for a small inset', () => {
|
|
128
|
+
// An inset under what the gap absorbs must not pull the bar down toward the
|
|
129
|
+
// window edge — the gap is clamped, and the footprint with it.
|
|
130
|
+
expect(footprintFor(8)).toBe(footprintFor(0));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('equals the space the rendered bar actually occupies', () => {
|
|
134
|
+
// The invariant that makes the hook worth exporting: its number is the bar's
|
|
135
|
+
// own geometry (the gap it holds off the window edge plus its expanded
|
|
136
|
+
// height), not a second copy that can drift.
|
|
137
|
+
mockInsets.bottom = HOME_INDICATOR_INSET;
|
|
138
|
+
let footprint: number | undefined;
|
|
139
|
+
const { UNSAFE_root } = render(
|
|
140
|
+
withTheme(
|
|
141
|
+
<>
|
|
142
|
+
<FootprintProbe onValue={(value) => (footprint = value)} />
|
|
143
|
+
<TabBar activeIndex={0}>
|
|
144
|
+
{ITEMS.map((item, index) => (
|
|
145
|
+
<TabBarButton key={item.name} item={item} index={index} />
|
|
146
|
+
))}
|
|
147
|
+
</TabBar>
|
|
148
|
+
</>,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const styles = styledNodes(UNSAFE_root);
|
|
153
|
+
// The wrapper holding the pill off the bottom edge — the only node with a
|
|
154
|
+
// bottom margin.
|
|
155
|
+
const gaps = styles.filter((style) => typeof style.marginBottom === 'number');
|
|
156
|
+
expect(gaps).toHaveLength(1);
|
|
157
|
+
// The pill itself: the animated capsule sizes BOTH its height and its
|
|
158
|
+
// horizontal inset, which nothing else in the bar does. The inset is written
|
|
159
|
+
// as the two long-form margins — `marginHorizontal` would be dropped
|
|
160
|
+
// silently on web, since RNW's `createReactDOMStyle` does not expand it (see
|
|
161
|
+
// `TabBarAnimatedShorthand.test.ts`).
|
|
162
|
+
const pills = styles.filter(
|
|
163
|
+
(style) =>
|
|
164
|
+
typeof style.height === 'number' &&
|
|
165
|
+
typeof style.marginLeft === 'number' &&
|
|
166
|
+
typeof style.marginRight === 'number',
|
|
167
|
+
);
|
|
168
|
+
expect(pills).toHaveLength(1);
|
|
169
|
+
|
|
170
|
+
expect(footprint).toBe(Number(gaps[0]?.marginBottom) + Number(pills[0]?.height));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -49,7 +49,7 @@ const NEUTRAL_AND_WEB_FILES = [
|
|
|
49
49
|
'tab-bar/shared.ts',
|
|
50
50
|
'tab-bar/TabBarBase.tsx',
|
|
51
51
|
'tab-bar/minimize-context.tsx',
|
|
52
|
-
'tab-bar/use-
|
|
52
|
+
'tab-bar/use-footprint.ts',
|
|
53
53
|
'tab-bar/surface.tsx',
|
|
54
54
|
'tab-bar/surface-solid.tsx',
|
|
55
55
|
'tab-bar/surface.web.tsx',
|
|
@@ -71,10 +71,14 @@ describe('ToastHost platform split', () => {
|
|
|
71
71
|
expect(styleModule).toMatch(
|
|
72
72
|
/export const WEB_POSITION_FIXED = 'fixed' as ViewStyle\['position'\]/,
|
|
73
73
|
);
|
|
74
|
-
// Exactly one cast in the module, and
|
|
74
|
+
// Exactly one cast in the module, and it is the only RUNTIME export.
|
|
75
75
|
// (Counted against code — the doc comment discusses casts in prose.)
|
|
76
|
+
// The module's other export is the `WebCssStyle` interface, which covers the
|
|
77
|
+
// other half of the same RN/RNW gap and emits nothing — see
|
|
78
|
+
// `web-css-style.test.ts`.
|
|
76
79
|
expect(code('styles/web-view-style.ts').match(/ as /g)).toHaveLength(1);
|
|
77
|
-
expect(styleModule.match(/^export /gm)).toHaveLength(1);
|
|
80
|
+
expect(styleModule.match(/^export const /gm)).toHaveLength(1);
|
|
81
|
+
expect(styleModule.match(/^export (?!const |interface )/gm)).toBeNull();
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
it('no engine file other than the host reaches for a platform overlay', () => {
|
|
@@ -2,8 +2,12 @@ import React from 'react';
|
|
|
2
2
|
import { AppState, Text } from 'react-native';
|
|
3
3
|
import { act, fireEvent, render } from '@testing-library/react-native';
|
|
4
4
|
|
|
5
|
+
import { space } from '../styles/tokens';
|
|
5
6
|
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ENTERING_ANIMATION_DURATION,
|
|
9
|
+
TOAST_MAX_ROW_WIDTH,
|
|
10
|
+
} from '../toast/constants';
|
|
7
11
|
import { toast, ToastOutlet } from '../toast';
|
|
8
12
|
import { toastStore } from '../toast/toast-store';
|
|
9
13
|
import type { ToasterProps, ToastPosition } from '../toast/types';
|
|
@@ -137,6 +141,16 @@ const anchorOf = ({ UNSAFE_root }: ReturnType<typeof renderOutlet>) =>
|
|
|
137
141
|
return style.position === 'absolute' && style.width === '100%';
|
|
138
142
|
})[0];
|
|
139
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The capped, centred row box — `ToastSwipeHandler`'s wrapper, the only row view
|
|
146
|
+
* carrying the `aria-live` announcement.
|
|
147
|
+
*/
|
|
148
|
+
const rowBoxOf = ({ UNSAFE_root }: ReturnType<typeof renderOutlet>) =>
|
|
149
|
+
UNSAFE_root.find(
|
|
150
|
+
(node) =>
|
|
151
|
+
hostName(node) === 'Animated.View' && node.props['aria-live'] !== undefined,
|
|
152
|
+
);
|
|
153
|
+
|
|
140
154
|
describe('ToastOutlet', () => {
|
|
141
155
|
beforeEach(() => {
|
|
142
156
|
jest.useFakeTimers();
|
|
@@ -382,6 +396,53 @@ describe('ToastOutlet', () => {
|
|
|
382
396
|
expect(toastStore.getSnapshot().toastHeights[id]).toBeUndefined();
|
|
383
397
|
});
|
|
384
398
|
|
|
399
|
+
/**
|
|
400
|
+
* W12 — the row box is capped so a toast does not span a desktop viewport.
|
|
401
|
+
* The cap sits on the swipe wrapper rather than on `ToastContent`'s card so it
|
|
402
|
+
* also covers `toast.custom` JSX and `unstyled` rows, which never render that
|
|
403
|
+
* card.
|
|
404
|
+
*/
|
|
405
|
+
describe('row width cap', () => {
|
|
406
|
+
it('caps and centres the row box', () => {
|
|
407
|
+
const rendered = renderOutlet();
|
|
408
|
+
show(() => toast('Not a banner'));
|
|
409
|
+
|
|
410
|
+
expect(flattenStyle(rowBoxOf(rendered).props.style)).toEqual(
|
|
411
|
+
expect.objectContaining({
|
|
412
|
+
width: '100%',
|
|
413
|
+
maxWidth: TOAST_MAX_ROW_WIDTH,
|
|
414
|
+
// Without this the capped row sits against the left edge: a child with
|
|
415
|
+
// a definite cross size is not stretched by the anchor.
|
|
416
|
+
alignSelf: 'center',
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('caps a custom-jsx row too, not just the default card', () => {
|
|
422
|
+
const rendered = renderOutlet();
|
|
423
|
+
show(() => toast.custom(<Text>Fully custom</Text>));
|
|
424
|
+
|
|
425
|
+
expect(flattenStyle(rowBoxOf(rendered).props.style).maxWidth).toBe(
|
|
426
|
+
TOAST_MAX_ROW_WIDTH,
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('leaves the card its gutters inside the cap', () => {
|
|
431
|
+
// The cap sizes the ROW, which carries the card's two `space.lg` gutters,
|
|
432
|
+
// so the visible card lands on sonner's 356px reference width.
|
|
433
|
+
expect(TOAST_MAX_ROW_WIDTH - space.lg * 2).toBe(356);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('lets a consumer widen the cap through toastContainerStyle', () => {
|
|
437
|
+
const rendered = renderOutlet({
|
|
438
|
+
toastOptions: { toastContainerStyle: { maxWidth: 600 } },
|
|
439
|
+
});
|
|
440
|
+
show(() => toast('Wider please'));
|
|
441
|
+
|
|
442
|
+
expect(flattenStyle(rowBoxOf(rendered).props.style).maxWidth).toBe(600);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
385
446
|
/**
|
|
386
447
|
* The outlet's `toastOptions` style slots and a per-toast `styles` object are
|
|
387
448
|
* folded into ONE styles object (`mergeToastStyles`). Upstream threads all
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ESTIMATED_TOAST_HEIGHT,
|
|
3
3
|
MIN_STACK_SCALE_X,
|
|
4
|
+
TOAST_MAX_ROW_WIDTH,
|
|
4
5
|
} from '../toast/constants';
|
|
5
6
|
import {
|
|
6
7
|
calculateStackScaleX,
|
|
@@ -206,7 +207,7 @@ describe('calculateStackScaleX', () => {
|
|
|
206
207
|
position = 'bottom-center' as ToastPosition,
|
|
207
208
|
isExpanded = false,
|
|
208
209
|
stackGap = 8,
|
|
209
|
-
|
|
210
|
+
rowWidth = 400,
|
|
210
211
|
}: {
|
|
211
212
|
index: number;
|
|
212
213
|
numberOfToasts?: number;
|
|
@@ -214,7 +215,7 @@ describe('calculateStackScaleX', () => {
|
|
|
214
215
|
position?: ToastPosition;
|
|
215
216
|
isExpanded?: boolean;
|
|
216
217
|
stackGap?: number;
|
|
217
|
-
|
|
218
|
+
rowWidth?: number;
|
|
218
219
|
}) =>
|
|
219
220
|
calculateStackScaleX({
|
|
220
221
|
index,
|
|
@@ -223,14 +224,14 @@ describe('calculateStackScaleX', () => {
|
|
|
223
224
|
position,
|
|
224
225
|
isExpanded,
|
|
225
226
|
stackGap,
|
|
226
|
-
|
|
227
|
+
rowWidth,
|
|
227
228
|
});
|
|
228
229
|
|
|
229
230
|
it.each([
|
|
230
231
|
['stacking is off', { index: 0, enableStacking: false }],
|
|
231
232
|
['there is only one row', { index: 0, numberOfToasts: 1 }],
|
|
232
233
|
['the stack is expanded', { index: 0, isExpanded: true }],
|
|
233
|
-
['the
|
|
234
|
+
['the row has not been measured', { index: 0, rowWidth: 0 }],
|
|
234
235
|
])('does not squeeze when %s', (_label, params) => {
|
|
235
236
|
expect(scaleAt(params)).toBe(1);
|
|
236
237
|
});
|
|
@@ -249,11 +250,26 @@ describe('calculateStackScaleX', () => {
|
|
|
249
250
|
|
|
250
251
|
it('never squeezes past MIN_STACK_SCALE_X', () => {
|
|
251
252
|
expect(
|
|
252
|
-
scaleAt({ index: 0, numberOfToasts: 50, stackGap: 40,
|
|
253
|
+
scaleAt({ index: 0, numberOfToasts: 50, stackGap: 40, rowWidth: 200 }),
|
|
253
254
|
).toBe(MIN_STACK_SCALE_X);
|
|
254
255
|
});
|
|
255
256
|
|
|
256
|
-
it('scales the squeeze with the
|
|
257
|
-
expect(scaleAt({ index: 1,
|
|
257
|
+
it('scales the squeeze with the row width', () => {
|
|
258
|
+
expect(scaleAt({ index: 1, rowWidth: 800 })).toBe(1 - 16 / 800);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The squeeze is measured against the ROW, which is capped, not the window.
|
|
263
|
+
* Feeding it a desktop window width instead is the bug this pins: the buried
|
|
264
|
+
* row would narrow by 4 visible pixels rather than the intended 16.
|
|
265
|
+
*/
|
|
266
|
+
it('squeezes a capped row by the same visible amount on a desktop viewport', () => {
|
|
267
|
+
const desktopWindowWidth = 1280;
|
|
268
|
+
expect(scaleAt({ index: 1, rowWidth: TOAST_MAX_ROW_WIDTH })).toBe(
|
|
269
|
+
1 - 16 / TOAST_MAX_ROW_WIDTH,
|
|
270
|
+
);
|
|
271
|
+
expect(scaleAt({ index: 1, rowWidth: TOAST_MAX_ROW_WIDTH })).toBeLessThan(
|
|
272
|
+
scaleAt({ index: 1, rowWidth: desktopWindowWidth }),
|
|
273
|
+
);
|
|
258
274
|
});
|
|
259
275
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sibling of `web-position-fixed.test.ts`, guarding the OTHER half of the RN/RNW
|
|
6
|
+
* type gap.
|
|
7
|
+
*
|
|
8
|
+
* Eleven sites across seven files carried an inline `{ … } as ViewStyle` to smuggle
|
|
9
|
+
* web-only CSS (`animation`, `transition*`, `backdrop-filter`, `mask-image`,
|
|
10
|
+
* `scrollbar-*`, `user-select`) past React Native's `ViewStyle`. That is not a
|
|
11
|
+
* widening, it is SILENCING: `as` only requires the two types to be COMPARABLE, so
|
|
12
|
+
* `{ animatoin: '…' } as ViewStyle` compiles and the typo ships as a style key
|
|
13
|
+
* react-native-web drops on the floor.
|
|
14
|
+
*
|
|
15
|
+
* They now annotate with `WebCssStyle`, which restores excess-property checking
|
|
16
|
+
* AND erases completely — unlike a widening helper function, an annotation emits
|
|
17
|
+
* nothing, so the compiled output is the same as the cast's. Two of the eleven
|
|
18
|
+
* needed no widening at all once checked against RN 0.83, which models
|
|
19
|
+
* `boxShadow` and `cursor` on `ViewStyle` directly.
|
|
20
|
+
*
|
|
21
|
+
* This guard stops an inline cast coming back. It pins the FULL remaining set —
|
|
22
|
+
* per file and per count — rather than an allowlist of filenames, so a new cast
|
|
23
|
+
* inside an already-listed file fails too.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const SRC = join(__dirname, '..');
|
|
27
|
+
|
|
28
|
+
function sourceFiles(dir: string): string[] {
|
|
29
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
30
|
+
const full = join(dir, entry);
|
|
31
|
+
if (statSync(full).isDirectory()) {
|
|
32
|
+
return entry === '__tests__' ? [] : sourceFiles(full);
|
|
33
|
+
}
|
|
34
|
+
return /\.tsx?$/.test(entry) ? [full] : [];
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Source with comments stripped — they legitimately discuss the retired spelling. */
|
|
39
|
+
const codeOf = (file: string) =>
|
|
40
|
+
readFileSync(file, 'utf8')
|
|
41
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
42
|
+
.replace(/^\s*\/\/.*$/gm, '');
|
|
43
|
+
|
|
44
|
+
const files = sourceFiles(SRC);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The only `as ViewStyle` occurrences left in `src/`, and why each one is NOT a
|
|
48
|
+
* web-CSS cast. Anything else is a regression.
|
|
49
|
+
*/
|
|
50
|
+
const ALLOWED: Record<string, { count: number; reason: string }> = {
|
|
51
|
+
'styles/web-view-style.ts': {
|
|
52
|
+
count: 1,
|
|
53
|
+
reason:
|
|
54
|
+
"`WEB_POSITION_FIXED` — a VALUE-level cast on a key RN does model, just with a narrower union. It is the one documented crossing point and cannot be an annotation, because `position: 'fixed'` has to be writable inside an ordinary style object.",
|
|
55
|
+
},
|
|
56
|
+
'skeleton/index.tsx': {
|
|
57
|
+
count: 1,
|
|
58
|
+
reason:
|
|
59
|
+
'`(flattened as ViewStyle)?.width` narrows a `StyleSheet.flatten` result, which is legitimately `ViewStyle | TextStyle | ImageStyle`. Not a web-CSS cast.',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe('web-only CSS styles', () => {
|
|
64
|
+
it('finds source files to scan (guards against a broken walk)', () => {
|
|
65
|
+
expect(files.length).toBeGreaterThan(100);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('has no inline `as ViewStyle` outside the two documented exceptions', () => {
|
|
69
|
+
const found: Record<string, number> = {};
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const hits = codeOf(file).match(/\bas ViewStyle\b/g);
|
|
72
|
+
if (hits) {
|
|
73
|
+
found[file.replace(`${SRC}/`, '')] = hits.length;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const expected = Object.fromEntries(
|
|
77
|
+
Object.entries(ALLOWED).map(([file, { count }]) => [file, count]),
|
|
78
|
+
);
|
|
79
|
+
expect(found).toEqual(expected);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The properties `WebCssStyle` exists for. If one of these shows up in source
|
|
84
|
+
* next to a cast again, the test above catches it; this one catches the subtler
|
|
85
|
+
* regression of re-declaring the same widening locally instead of importing it.
|
|
86
|
+
*/
|
|
87
|
+
it('declares every web-only property in one place', () => {
|
|
88
|
+
const module = codeOf(join(SRC, 'styles/web-view-style.ts'));
|
|
89
|
+
for (const property of [
|
|
90
|
+
'animation',
|
|
91
|
+
'animationDelay',
|
|
92
|
+
'transitionProperty',
|
|
93
|
+
'transitionDuration',
|
|
94
|
+
'transitionTimingFunction',
|
|
95
|
+
'backdropFilter',
|
|
96
|
+
'WebkitBackdropFilter',
|
|
97
|
+
'maskImage',
|
|
98
|
+
'WebkitMaskImage',
|
|
99
|
+
'backgroundImage',
|
|
100
|
+
'scrollbarWidth',
|
|
101
|
+
'scrollbarColor',
|
|
102
|
+
'userSelect',
|
|
103
|
+
]) {
|
|
104
|
+
expect(module).toContain(`${property}?:`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('is a type-only import everywhere it is used, so nothing is emitted', () => {
|
|
109
|
+
const importers = files.filter(
|
|
110
|
+
(file) =>
|
|
111
|
+
file !== join(SRC, 'styles/web-view-style.ts') &&
|
|
112
|
+
codeOf(file).includes('WebCssStyle'),
|
|
113
|
+
);
|
|
114
|
+
// The seven forks the sweep converted.
|
|
115
|
+
expect(importers.length).toBeGreaterThanOrEqual(7);
|
|
116
|
+
for (const file of importers) {
|
|
117
|
+
const source = codeOf(file);
|
|
118
|
+
// Either `import type { WebCssStyle }` or an inline `type` specifier.
|
|
119
|
+
expect({
|
|
120
|
+
file: file.replace(`${SRC}/`, ''),
|
|
121
|
+
typeOnly:
|
|
122
|
+
/import type \{[^}]*\bWebCssStyle\b/.test(source) ||
|
|
123
|
+
/\btype WebCssStyle\b/.test(source),
|
|
124
|
+
}).toEqual({ file: file.replace(`${SRC}/`, ''), typeOnly: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* RN 0.83 models these on `ViewStyle` itself, so widening them would be cargo
|
|
130
|
+
* cult. Pinned because getting it wrong is invisible: the cast compiles either
|
|
131
|
+
* way and only the reader is misled.
|
|
132
|
+
*/
|
|
133
|
+
it('does not widen properties React Native already models', () => {
|
|
134
|
+
const module = codeOf(join(SRC, 'styles/web-view-style.ts'));
|
|
135
|
+
for (const property of ['cursor', 'boxShadow', 'filter', 'pointerEvents']) {
|
|
136
|
+
expect(module).not.toContain(`${property}?:`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type StyleProp,
|
|
12
12
|
} from 'react-native';
|
|
13
13
|
import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler';
|
|
14
|
+
import type { WebCssStyle } from '../styles/web-view-style';
|
|
14
15
|
import { Z_INDEX } from '../styles/z-index';
|
|
15
16
|
import Animated, {
|
|
16
17
|
type AnimatedStyle,
|
|
@@ -737,19 +738,22 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
737
738
|
</GestureDetector>
|
|
738
739
|
) : handleNode;
|
|
739
740
|
|
|
741
|
+
// `scrollbar-width` / `scrollbar-color` are web-only CSS RN does not model.
|
|
742
|
+
const webScrollbarStyle: WebCssStyle | undefined =
|
|
743
|
+
Platform.OS === 'web'
|
|
744
|
+
? {
|
|
745
|
+
scrollbarWidth: 'thin',
|
|
746
|
+
scrollbarColor: `${colors.border} transparent`,
|
|
747
|
+
}
|
|
748
|
+
: undefined;
|
|
749
|
+
|
|
740
750
|
// Inner content: scrollable wraps in Animated.ScrollView, non-scrollable
|
|
741
751
|
// renders children directly. In legacy mode the scrollview is also
|
|
742
752
|
// wrapped in the `nativeGesture` detector for scroll/pan coordination.
|
|
743
753
|
const scrollViewNode = (
|
|
744
754
|
<Animated.ScrollView
|
|
745
755
|
ref={scrollViewRef}
|
|
746
|
-
style={[
|
|
747
|
-
styles.scrollView,
|
|
748
|
-
Platform.OS === 'web' && ({
|
|
749
|
-
scrollbarWidth: 'thin',
|
|
750
|
-
scrollbarColor: `${colors.border} transparent`,
|
|
751
|
-
} as ViewStyle),
|
|
752
|
-
]}
|
|
756
|
+
style={[styles.scrollView, webScrollbarStyle]}
|
|
753
757
|
contentContainerStyle={dynamicStyles.scrollContent}
|
|
754
758
|
showsVerticalScrollIndicator={false}
|
|
755
759
|
keyboardShouldPersistTaps="handled"
|