@oxyhq/bloom 1.14.0 → 1.14.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/docs/bottom-sheet.mdx +1 -0
- package/docs/media-flight.mdx +15 -6
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +4 -0
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/media-flight/MediaSurface.js +51 -10
- package/lib/commonjs/media-flight/MediaSurface.js.map +1 -1
- package/lib/commonjs/media-flight/constants.js +32 -7
- package/lib/commonjs/media-flight/constants.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +34 -6
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/theme/color-scope/ColorScope.js +42 -12
- package/lib/commonjs/theme/color-scope/ColorScope.js.map +1 -1
- package/lib/commonjs/theme/color-scope/ColorScope.web.js +2 -8
- package/lib/commonjs/theme/color-scope/ColorScope.web.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +5 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/media-flight/MediaSurface.js +52 -11
- package/lib/module/media-flight/MediaSurface.js.map +1 -1
- package/lib/module/media-flight/constants.js +31 -6
- package/lib/module/media-flight/constants.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +35 -7
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/theme/color-scope/ColorScope.js +45 -13
- package/lib/module/theme/color-scope/ColorScope.js.map +1 -1
- package/lib/module/theme/color-scope/ColorScope.web.js +3 -9
- package/lib/module/theme/color-scope/ColorScope.web.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/types.d.ts +6 -0
- package/lib/typescript/commonjs/bottom-sheet/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts.map +1 -1
- package/lib/typescript/commonjs/media-flight/constants.d.ts +30 -6
- package/lib/typescript/commonjs/media-flight/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/media-flight/types.d.ts +7 -2
- package/lib/typescript/commonjs/media-flight/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/ColorScope.d.ts +36 -2
- package/lib/typescript/commonjs/theme/color-scope/ColorScope.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/ColorScope.web.d.ts +6 -2
- package/lib/typescript/commonjs/theme/color-scope/ColorScope.web.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/types.d.ts +6 -0
- package/lib/typescript/module/bottom-sheet/types.d.ts.map +1 -1
- package/lib/typescript/module/media-flight/MediaSurface.d.ts.map +1 -1
- package/lib/typescript/module/media-flight/constants.d.ts +30 -6
- package/lib/typescript/module/media-flight/constants.d.ts.map +1 -1
- package/lib/typescript/module/media-flight/types.d.ts +7 -2
- package/lib/typescript/module/media-flight/types.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/ColorScope.d.ts +36 -2
- package/lib/typescript/module/theme/color-scope/ColorScope.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/ColorScope.web.d.ts +6 -2
- package/lib/typescript/module/theme/color-scope/ColorScope.web.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/__tests__/support/press-host.ts +30 -0
- package/src/__tests__/support/rendered-style.ts +99 -0
- package/src/__tests__/support/unread-hook-fixture.ts +33 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +13 -0
- package/src/bottom-sheet/types.ts +6 -0
- package/src/media-flight/MediaSurface.tsx +60 -14
- package/src/media-flight/constants.ts +31 -6
- package/src/media-flight/types.ts +7 -2
- package/src/tab-bar/TabBarBase.tsx +41 -7
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +7682 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
- package/src/theme/color-scope/ColorScope.tsx +53 -12
- package/src/theme/color-scope/ColorScope.web.tsx +10 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/media-flight/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE7E;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,eAAe,CACb,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GACtE,IAAI,CAAC;CACT;AAED,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,kEAAkE;IAClE,MAAM,EAAE,eAAe,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAExE;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,eAAe,CAAE,SAAQ,iBAAiB;IACvF,MAAM,EAAE,CAAC,CAAC;CACX;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,mBAAmB,CAAC;IAC7B,wCAAwC;IACxC,IAAI,EAAE,YAAY,CAAC;IACnB,oCAAoC;IACpC,EAAE,EAAE,YAAY,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC;IAChC,gFAAgF;IAChF,WAAW,EAAE,gBAAgB,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;OAWG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wCAAwC;AACxC,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,GAAG,IAAI,KAAK,IAAI,CAAC;IACzE;;;;OAIG;IACH,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC5D;;;;;;;;;;;;;OAaG;IACH,KAAK,EAAE,CACL,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,kBAAkB,KACzB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB;;;;OAIG;IACH,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B;;;;;;;;;OASG;IACH,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B;;;;;;OAMG;IACH,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe;IAC9E;;;;;;;;;OASG;IACH,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IACjB,4CAA4C;IAC5C,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5B,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC;CACjC;AAED
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/media-flight/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TabBarBase.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/TabBarBase.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"TabBarBase.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/TabBarBase.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,OAAO,CAAC;AA2Bf,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,OAAO,EAiBL,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAe,MAAM,SAAS,CAAC;AA4oB3E;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAC1C,IAAI,EAAE,aAAa,CAAC,oBAAoB,CAAC;YAElB,WAAW;;EAGnC;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,CAAC,gBAAgB,CAAC;YAC1C,iBAAiB;;EAG/C"}
|
|
@@ -3,8 +3,13 @@ import { type StyleProp, type ViewStyle } from 'react-native';
|
|
|
3
3
|
import type { AppColorName } from '../color-presets';
|
|
4
4
|
export interface BloomColorScopeProps {
|
|
5
5
|
/**
|
|
6
|
-
* Preset to apply within this subtree. When `undefined`, the scope
|
|
7
|
-
*
|
|
6
|
+
* Preset to apply within this subtree. When `undefined`, the scope publishes
|
|
7
|
+
* nothing of its own and children inherit the parent scope's preset unchanged.
|
|
8
|
+
*
|
|
9
|
+
* It is a no-op in what it PUBLISHES, never in what it RENDERS — see the
|
|
10
|
+
* component's own comment: the element tree this returns is identical whether
|
|
11
|
+
* or not a preset is set, because a preset that arrives late must not remount
|
|
12
|
+
* the subtree.
|
|
8
13
|
*/
|
|
9
14
|
colorPreset: AppColorName | undefined;
|
|
10
15
|
/**
|
|
@@ -16,6 +21,35 @@ export interface BloomColorScopeProps {
|
|
|
16
21
|
style?: StyleProp<ViewStyle>;
|
|
17
22
|
children: React.ReactNode;
|
|
18
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* A subtree themed by one color preset.
|
|
26
|
+
*
|
|
27
|
+
* THE SHAPE OF WHAT THIS RETURNS DOES NOT DEPEND ON `colorPreset`. Both
|
|
28
|
+
* providers are mounted unconditionally, and an absent preset is expressed as
|
|
29
|
+
* pass-through VALUES ({@link NO_VARS}, the parent's own theme) rather than as a
|
|
30
|
+
* shorter tree. That is a correctness requirement, not a tidiness one, and it
|
|
31
|
+
* cost a real bug to learn:
|
|
32
|
+
*
|
|
33
|
+
* The no-preset path used to be `return <>{children}</>` — two provider levels
|
|
34
|
+
* shallower than the preset path. React reconciles by POSITION, so the first
|
|
35
|
+
* render after a preset arrives finds different element types at that position
|
|
36
|
+
* and unmounts the whole subtree to mount a new one. Every consumer that resolves
|
|
37
|
+
* its preset asynchronously — a profile screen learning the account's color a
|
|
38
|
+
* few hundred milliseconds after it opens — therefore remounted everything below
|
|
39
|
+
* the scope, including its own navigator.
|
|
40
|
+
*
|
|
41
|
+
* In Mention that scope sits above the app's route stack: the remount rebuilt
|
|
42
|
+
* the `[username]` navigator from scratch, which dropped the pushed child route
|
|
43
|
+
* AND its `username` param, and the profile a reader had just tapped rendered
|
|
44
|
+
* itself as "Profile not found" (mention#…, reproduced from the reel on a Pixel
|
|
45
|
+
* 10 Pro; the A/B was pinning `colorPreset` and watching the collapse stop).
|
|
46
|
+
* Remounting also throws away scroll positions, video players and in-flight
|
|
47
|
+
* state, so the same bug was costing far more than the one visible symptom.
|
|
48
|
+
*
|
|
49
|
+
* If a future change wants the no-preset case to render less, it has to keep the
|
|
50
|
+
* element types at this position identical across a preset change, or the
|
|
51
|
+
* remount comes back.
|
|
52
|
+
*/
|
|
19
53
|
export declare function BloomColorScope({ colorPreset, asChild, style, children, }: BloomColorScopeProps): import("react/jsx-runtime").JSX.Element;
|
|
20
54
|
/**
|
|
21
55
|
* Escape hatch for advanced cases where the wrapping element is owned by the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColorScope.d.ts","sourceRoot":"","sources":["../../../../../src/theme/color-scope/ColorScope.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsE,MAAM,OAAO,CAAC;AAC3F,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"ColorScope.d.ts","sourceRoot":"","sources":["../../../../../src/theme/color-scope/ColorScope.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsE,MAAM,OAAO,CAAC;AAC3F,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAarD,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,WAAW,EAAE,YAAY,GAAG,SAAS,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0GAA0G;IAC1G,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,OAAe,EACf,KAAK,EACL,QAAQ,GACT,EAAE,oBAAoB,2CAyDtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,YAAY,GAAG,SAAS,CAAC,SAAS,CAAC,CAYlF"}
|
|
@@ -2,8 +2,12 @@ import React from 'react';
|
|
|
2
2
|
import type { AppColorName } from '../color-presets';
|
|
3
3
|
export interface BloomColorScopeProps {
|
|
4
4
|
/**
|
|
5
|
-
* Preset to apply within this subtree. When `undefined`, the scope
|
|
6
|
-
*
|
|
5
|
+
* Preset to apply within this subtree. When `undefined`, the scope publishes
|
|
6
|
+
* nothing of its own and children inherit the parent scope's preset unchanged.
|
|
7
|
+
*
|
|
8
|
+
* It is a no-op in what it PUBLISHES, never in what it RENDERS — the element
|
|
9
|
+
* tree is identical either way, so a preset that arrives late cannot remount
|
|
10
|
+
* the subtree. `ColorScope.tsx` carries the full account.
|
|
7
11
|
*/
|
|
8
12
|
colorPreset: AppColorName | undefined;
|
|
9
13
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColorScope.web.d.ts","sourceRoot":"","sources":["../../../../../src/theme/color-scope/ColorScope.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsE,MAAM,OAAO,CAAC;AAI3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAgBrD,MAAM,WAAW,oBAAoB;IACnC
|
|
1
|
+
{"version":3,"file":"ColorScope.web.d.ts","sourceRoot":"","sources":["../../../../../src/theme/color-scope/ColorScope.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsE,MAAM,OAAO,CAAC;AAI3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAgBrD,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,WAAW,EAAE,YAAY,GAAG,SAAS,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yGAAyG;IACzG,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AA+BD,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,OAAe,EACf,KAAK,EACL,QAAQ,GACT,EAAE,oBAAoB,2CAoDtB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,YAAY,GAAG,KAAK,CAAC,aAAa,CAYjF"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A barrel that DELIBERATELY offers one name from two declarations, so that
|
|
3
|
+
* `barrel-name-collisions.test.ts` can prove its detector fires. It reproduces
|
|
4
|
+
* the shipped `Item` shape: a star export carrying `Item` from `item/`, and an
|
|
5
|
+
* explicit re-export binding that same name to a different component.
|
|
6
|
+
*
|
|
7
|
+
* Nothing imports this at runtime. bob's `exclude` keeps it out of `lib/`, and
|
|
8
|
+
* `files: ["src", …]` does ship it inside the tarball's `src/` like the other
|
|
9
|
+
* test files there — but no entry point reaches it, so no bundler links it, and
|
|
10
|
+
* `package.json#exports` never names it.
|
|
11
|
+
*
|
|
12
|
+
* The `Card` lines below are the other half of the
|
|
13
|
+
* control: one name offered twice from ONE declaration, which the detector must
|
|
14
|
+
* stay quiet about — otherwise its cheapest fix would be deleting a legitimate
|
|
15
|
+
* redundant re-export.
|
|
16
|
+
*/
|
|
17
|
+
export * from '../../item';
|
|
18
|
+
export { Card as Item } from '../../card';
|
|
19
|
+
export { Card } from '../../card';
|
|
20
|
+
export * from '../../card';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A spec-shaped stand-in for constructed stylesheets, for the jsdom suites.
|
|
3
|
+
*
|
|
4
|
+
* jsdom 26 ships the `CSSStyleSheet` CONSTRUCTOR but neither `replaceSync` nor
|
|
5
|
+
* `document.adoptedStyleSheets`, so every jsdom test takes Bloom's `<style>`
|
|
6
|
+
* fallback. That makes the whole CSP-safe path — the reason
|
|
7
|
+
* `styles/adopt-style-sheet.ts` exists — invisible to jest unless a test
|
|
8
|
+
* installs the API itself. Without this, a mutation that deleted the adoption
|
|
9
|
+
* branch entirely would leave the suite green.
|
|
10
|
+
*
|
|
11
|
+
* Not collected as a suite: jest's `testMatch` wants `*.test.ts` / `*.spec.ts`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface FakeStyleSheet {
|
|
15
|
+
/** The CSS most recently handed to `replaceSync`. */
|
|
16
|
+
cssText: string;
|
|
17
|
+
/** How many times the sheet has been re-parsed, so a test can prove it wasn't. */
|
|
18
|
+
replaceSyncCalls: number;
|
|
19
|
+
replaceSync(css: string): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ConstructedStyleSheetsHarness {
|
|
23
|
+
/** The document's adopted sheets, read live (the code reassigns the array). */
|
|
24
|
+
adopted(): readonly FakeStyleSheet[];
|
|
25
|
+
/** Drop the API again, restoring jsdom's own `CSSStyleSheet`. */
|
|
26
|
+
uninstall(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function installConstructedStyleSheets(): ConstructedStyleSheetsHarness {
|
|
30
|
+
class FakeCSSStyleSheet implements FakeStyleSheet {
|
|
31
|
+
cssText = '';
|
|
32
|
+
replaceSyncCalls = 0;
|
|
33
|
+
|
|
34
|
+
replaceSync(css: string): void {
|
|
35
|
+
this.cssText = css;
|
|
36
|
+
this.replaceSyncCalls += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const previousConstructor = Object.getOwnPropertyDescriptor(
|
|
41
|
+
globalThis,
|
|
42
|
+
'CSSStyleSheet',
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
Object.defineProperty(globalThis, 'CSSStyleSheet', {
|
|
46
|
+
value: FakeCSSStyleSheet,
|
|
47
|
+
writable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(document, 'adoptedStyleSheets', {
|
|
51
|
+
value: [],
|
|
52
|
+
writable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
adopted: () =>
|
|
58
|
+
document.adoptedStyleSheets as unknown as readonly FakeStyleSheet[],
|
|
59
|
+
uninstall: () => {
|
|
60
|
+
Reflect.deleteProperty(document, 'adoptedStyleSheets');
|
|
61
|
+
if (previousConstructor) {
|
|
62
|
+
Object.defineProperty(globalThis, 'CSSStyleSheet', previousConstructor);
|
|
63
|
+
} else {
|
|
64
|
+
Reflect.deleteProperty(globalThis, 'CSSStyleSheet');
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pressing a component's OWN host node, and proving the component is what
|
|
3
|
+
* installed the handler being measured.
|
|
4
|
+
*
|
|
5
|
+
* `fireEvent.press` walks UP from the element it is handed to the nearest
|
|
6
|
+
* ancestor carrying an `onPress` prop, and it does not stop at host nodes — a
|
|
7
|
+
* COMPOSITE counts, because `getEventHandler` only reads `element.props`. So
|
|
8
|
+
* `<Thing onPress={fn}>` written in a test's own JSX catches the press itself.
|
|
9
|
+
* Measured on `ProfileCard`: with the handler deleted from the component the
|
|
10
|
+
* card rendered as an inert `View`, and the test still reported exactly one
|
|
11
|
+
* call. Green, and measuring nothing. Nine suites had the same shape.
|
|
12
|
+
*
|
|
13
|
+
* Asserting the host node's own `onPress` FIRST closes both halves:
|
|
14
|
+
*
|
|
15
|
+
* - a component that installs no handler at all fails here, and
|
|
16
|
+
* - a component that installs one which drops the caller's fails on the call
|
|
17
|
+
* count, because `fireEvent` now finds a handler AT the node and never walks
|
|
18
|
+
* past it to the test's own JSX.
|
|
19
|
+
*
|
|
20
|
+
* `host` must therefore be the node the component itself made pressable — the
|
|
21
|
+
* one carrying its `testID` / `accessibilityLabel` — not a `Text` deep inside
|
|
22
|
+
* it, or the walk-up is back and so is the hole.
|
|
23
|
+
*/
|
|
24
|
+
import { fireEvent } from '@testing-library/react-native';
|
|
25
|
+
import type { ReactTestInstance } from 'react-test-renderer';
|
|
26
|
+
|
|
27
|
+
export function pressHost(host: ReactTestInstance): void {
|
|
28
|
+
expect(typeof host.props.onPress).toBe('function');
|
|
29
|
+
fireEvent.press(host);
|
|
30
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading what actually landed on a rendered node — shared by every suite that
|
|
3
|
+
* asserts the "className lands on the node the parent lays out" rule.
|
|
4
|
+
*
|
|
5
|
+
* Three components had the same two-node defect (`Button`, then `Fab` and
|
|
6
|
+
* `FrostedIconButton`), and asserting it needs the same two things each time:
|
|
7
|
+
* the HOST tree (composites do not appear in `toJSON()`, so a wrapper is only
|
|
8
|
+
* visible there) and a deep flatten of the `style` prop, because react-native-css
|
|
9
|
+
* merges a caller's class in as a `{ $$css: true, className }` descriptor
|
|
10
|
+
* ALONGSIDE the style objects rather than in a prop of its own.
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactTestRendererJSON } from 'react-test-renderer';
|
|
13
|
+
|
|
14
|
+
export type StyleEntry = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
export interface HostNode {
|
|
17
|
+
type: string;
|
|
18
|
+
props: Record<string, unknown>;
|
|
19
|
+
children: Array<HostNode | string> | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Deep-flatten an RN `style` prop (nested arrays, `false`/`null` holes) into the
|
|
24
|
+
* list of style objects that actually reached the node.
|
|
25
|
+
*/
|
|
26
|
+
export function styleEntries(style: unknown, out: StyleEntry[] = []): StyleEntry[] {
|
|
27
|
+
if (!style || typeof style !== 'object') return out;
|
|
28
|
+
if (Array.isArray(style)) {
|
|
29
|
+
for (const entry of style) styleEntries(entry, out);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
out.push(style as StyleEntry);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Every style key/value that landed on a node, later entries winning. */
|
|
37
|
+
export function resolvedStyle(style: unknown): StyleEntry {
|
|
38
|
+
return Object.assign({}, ...styleEntries(style)) as StyleEntry;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The class tokens react-native-css accepted for a node, in arrival order. */
|
|
42
|
+
export function classNamesOn(style: unknown): string[] {
|
|
43
|
+
return styleEntries(style)
|
|
44
|
+
.filter((entry) => entry.$$css === true && typeof entry.className === 'string')
|
|
45
|
+
.map((entry) => String(entry.className));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isHostNode(value: unknown): value is HostNode {
|
|
49
|
+
return (
|
|
50
|
+
typeof value === 'object' && value !== null && typeof (value as HostNode).type === 'string'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Walk the HOST tree (composites are absent from `toJSON()`) for a testID. */
|
|
55
|
+
export function findHost(
|
|
56
|
+
node: ReactTestRendererJSON | ReactTestRendererJSON[] | unknown,
|
|
57
|
+
testID: string,
|
|
58
|
+
): HostNode | null {
|
|
59
|
+
if (Array.isArray(node)) {
|
|
60
|
+
for (const child of node) {
|
|
61
|
+
const hit = findHost(child, testID);
|
|
62
|
+
if (hit) return hit;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (!isHostNode(node)) return null;
|
|
67
|
+
if (node.props.testID === testID) return node;
|
|
68
|
+
return findHost(node.children, testID);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every host node in a rendered tree, in document order.
|
|
73
|
+
*
|
|
74
|
+
* The reason to walk `toJSON()` rather than `UNSAFE_root.findAll` is that the
|
|
75
|
+
* latter yields COMPOSITE instances too, so a `memo(fn)` component contributes
|
|
76
|
+
* two matches for one rendered element and every count is silently doubled —
|
|
77
|
+
* which reads as "the component rendered the thing twice", not as an artefact
|
|
78
|
+
* of the query.
|
|
79
|
+
*/
|
|
80
|
+
export function hostNodes(tree: unknown, out: HostNode[] = []): HostNode[] {
|
|
81
|
+
if (Array.isArray(tree)) {
|
|
82
|
+
for (const child of tree) hostNodes(child, out);
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
if (!isHostNode(tree)) return out;
|
|
86
|
+
out.push(tree);
|
|
87
|
+
return hostNodes(tree.children, out);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The host nodes a parent laid out. Callers assert the LENGTH themselves —
|
|
92
|
+
* "exactly one" is the property, and a helper that returned only the first would
|
|
93
|
+
* hide the second.
|
|
94
|
+
*/
|
|
95
|
+
export function renderedChildren(tree: unknown, hostTestID: string): HostNode[] {
|
|
96
|
+
const host = findHost(tree, hostTestID);
|
|
97
|
+
if (host === null) throw new Error(`no host rendered for testID "${hostTestID}"`);
|
|
98
|
+
return (host.children ?? []).filter(isHostNode);
|
|
99
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A module that DELIBERATELY throws away one of the values a hook hands back, so
|
|
3
|
+
* `hook-subscriptions-are-read.test.ts` can prove its detector fires. It carries
|
|
4
|
+
* both halves of the control: `unreadFlag` is bound and never read, which the
|
|
5
|
+
* detector must report, and `readFlag` is bound and read, which it must stay
|
|
6
|
+
* quiet about — otherwise the cheapest way to green the gate would be deleting a
|
|
7
|
+
* binding that is doing its job.
|
|
8
|
+
*
|
|
9
|
+
* Nothing imports this at runtime. bob's `exclude` keeps it out of `lib/`, and
|
|
10
|
+
* `files: ["src", …]` does ship it inside the tarball's `src/` like the other
|
|
11
|
+
* 137 test files there — but no entry point reaches it, so no bundler links it.
|
|
12
|
+
* It is a plain `.ts` with no React import because the
|
|
13
|
+
* detector reads source, never types: a call named `use…` bound to a name is all
|
|
14
|
+
* the shape there is.
|
|
15
|
+
*
|
|
16
|
+
* The destructuring RENAMES, which is not decoration — it reproduces the shipped
|
|
17
|
+
* shape (`const { state: pressed, onIn: onPressIn } = useInteractionState()`) and
|
|
18
|
+
* it is what keeps the control honest. A hook declared in the same file as its
|
|
19
|
+
* caller mentions its own property names in its return type and its return
|
|
20
|
+
* object, and the detector counts every occurrence of a spelling as a possible
|
|
21
|
+
* read, so an unrenamed fixture would be silently unreportable for a reason no
|
|
22
|
+
* real call site has.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Stands in for a real subscription — a hook by name, with two return values. */
|
|
26
|
+
function useFixtureInteractionState(): { first: boolean; second: boolean } {
|
|
27
|
+
return { first: false, second: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function fixtureComponentBody(): boolean {
|
|
31
|
+
const { first: readFlag, second: unreadFlag } = useFixtureInteractionState();
|
|
32
|
+
return readFlag;
|
|
33
|
+
}
|
|
@@ -20,6 +20,7 @@ import Animated, {
|
|
|
20
20
|
runOnJS,
|
|
21
21
|
type SharedValue,
|
|
22
22
|
useAnimatedScrollHandler,
|
|
23
|
+
useAnimatedReaction,
|
|
23
24
|
useAnimatedStyle,
|
|
24
25
|
useDerivedValue,
|
|
25
26
|
useSharedValue,
|
|
@@ -70,6 +71,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
70
71
|
scrollable = true,
|
|
71
72
|
manualActivation = false,
|
|
72
73
|
dynamicBackdrop = false,
|
|
74
|
+
animatedProgress,
|
|
73
75
|
handleComponent,
|
|
74
76
|
scrollY: externalScrollY,
|
|
75
77
|
headerOverlay,
|
|
@@ -509,6 +511,17 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
509
511
|
return opacity.value * dragFactor;
|
|
510
512
|
}, [dynamicBackdrop, opacity, translateY, screenHeightSV]);
|
|
511
513
|
|
|
514
|
+
useAnimatedReaction(
|
|
515
|
+
() => Math.min(
|
|
516
|
+
opacity.value,
|
|
517
|
+
interpolate(translateY.value, [0, screenHeightSV.value], [1, 0], 'clamp'),
|
|
518
|
+
),
|
|
519
|
+
(progress) => {
|
|
520
|
+
if (animatedProgress) animatedProgress.value = progress;
|
|
521
|
+
},
|
|
522
|
+
[animatedProgress, opacity, translateY, screenHeightSV],
|
|
523
|
+
);
|
|
524
|
+
|
|
512
525
|
// Only the consumer-supplied `backdropComponent` needs the progress as a
|
|
513
526
|
// style — it owns its own visuals, blur included, so the ancestor-opacity
|
|
514
527
|
// hazard is its call to make.
|
|
@@ -87,6 +87,12 @@ export interface BottomSheetProps {
|
|
|
87
87
|
* Defaults to `false` (constant opacity during drag).
|
|
88
88
|
*/
|
|
89
89
|
dynamicBackdrop?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Optional UI-thread progress mirror for coordinating content behind the
|
|
92
|
+
* sheet. `0` means fully closed and `1` fully presented; dragging the sheet
|
|
93
|
+
* updates every intermediate value without crossing the JS bridge.
|
|
94
|
+
*/
|
|
95
|
+
animatedProgress?: SharedValue<number>;
|
|
90
96
|
/**
|
|
91
97
|
* Custom handle slot. When provided, replaces the default drag handle
|
|
92
98
|
* (the 36×5 pill). The rendered handle is wrapped in the dedicated handle
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
import { Image } from 'expo-image';
|
|
26
26
|
import Animated from 'react-native-reanimated';
|
|
27
27
|
|
|
28
|
-
import { SLOT_IDENTITY_CHURN_LIMIT } from './constants';
|
|
28
|
+
import { SLOT_IDENTITY_CHURN_LIMIT, SLOT_IDENTITY_CHURN_WINDOW_MS } from './constants';
|
|
29
29
|
import { handOffFlight } from './store';
|
|
30
30
|
import type { MediaSurfaceContent, MediaVideoSlot } from './types';
|
|
31
31
|
import {
|
|
@@ -149,17 +149,61 @@ export const MediaSurface = memo(function MediaSurface({
|
|
|
149
149
|
// supports.
|
|
150
150
|
const [mountedSurfaceType] = useState(surfaceType);
|
|
151
151
|
|
|
152
|
-
// A slot rebuilt on every render
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
152
|
+
// A slot rebuilt on every render re-renders this surface — and on web
|
|
153
|
+
// republishes the shared node — every time the row it lives in renders, which
|
|
154
|
+
// nothing at runtime reports: the picture is correct and the app is just
|
|
155
|
+
// doing work in proportion to its render count.
|
|
156
|
+
//
|
|
157
|
+
// What is counted is a RUN of consecutive commits that each carried a new
|
|
158
|
+
// slot, NOT how many times the slot has ever changed. The lifetime count
|
|
159
|
+
// cannot tell the defect from a correctly memoised slot: Bloom's own
|
|
160
|
+
// documented example lists `isWatched` in its deps, so its identity changes
|
|
161
|
+
// once per activation of a long-lived surface, and any such consumer reaches
|
|
162
|
+
// any fixed lifetime total eventually. It would then be told to do the thing
|
|
163
|
+
// it is already doing, with no way to make the warning stop — which is worse
|
|
164
|
+
// than not warning, because the advice is unsatisfiable. A slot rebuilt per
|
|
165
|
+
// render is different in kind: it is new on EVERY commit, including the ones
|
|
166
|
+
// where nothing it reads changed.
|
|
167
|
+
//
|
|
168
|
+
// The run must also be FAST, and that second condition is what covers the
|
|
169
|
+
// consumer who memoised everything else too. With `content` and the rest
|
|
170
|
+
// stable, `memo` skips the renders where only the parent changed, so this
|
|
171
|
+
// component commits ONLY when the slot changes — every commit carries a new
|
|
172
|
+
// slot and the run never breaks, even though there is nothing to fix and no
|
|
173
|
+
// cost to pay. Rate is what tells them apart, and it is what the warning was
|
|
174
|
+
// ever about: a slot rebuilt per render churns as fast as its row renders,
|
|
175
|
+
// while one rebuilt when its state changes moves at the speed of a finger.
|
|
176
|
+
//
|
|
177
|
+
// All three refs are written from effects rather than in render: a
|
|
178
|
+
// render-phase ref write makes the React Compiler bail on the whole
|
|
179
|
+
// component. The first effect fires only when the identity changed, and the
|
|
180
|
+
// second — declared after it, and with no dependency array, so it runs on
|
|
181
|
+
// every commit — is what reads that mark and either extends the run or
|
|
182
|
+
// clears it.
|
|
183
|
+
const slotChangedInCommit = useRef(false);
|
|
184
|
+
const slotChurnRun = useRef(0);
|
|
185
|
+
const slotChurnRunStartedAt = useRef(0);
|
|
158
186
|
useEffect(() => {
|
|
159
|
-
|
|
160
|
-
slotChurn.current += 1;
|
|
161
|
-
if (slotChurn.current === SLOT_IDENTITY_CHURN_LIMIT) warnSlotNotMemoised();
|
|
187
|
+
slotChangedInCommit.current = true;
|
|
162
188
|
}, [renderVideo]);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (renderVideo === undefined || !slotChangedInCommit.current) {
|
|
191
|
+
slotChurnRun.current = 0;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
slotChangedInCommit.current = false;
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
if (
|
|
197
|
+
slotChurnRun.current === 0 ||
|
|
198
|
+
now - slotChurnRunStartedAt.current > SLOT_IDENTITY_CHURN_WINDOW_MS
|
|
199
|
+
) {
|
|
200
|
+
slotChurnRun.current = 1;
|
|
201
|
+
slotChurnRunStartedAt.current = now;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
slotChurnRun.current += 1;
|
|
205
|
+
if (slotChurnRun.current === SLOT_IDENTITY_CHURN_LIMIT) warnSlotNotMemoised();
|
|
206
|
+
});
|
|
163
207
|
|
|
164
208
|
// Both arms report the same fact — "there is a picture here now" — because the
|
|
165
209
|
// destination of a flight can be either, and a caller should not have to know
|
|
@@ -252,10 +296,12 @@ function warnSlotNotMemoised(): void {
|
|
|
252
296
|
hasWarnedAboutSlotChurn = true;
|
|
253
297
|
// eslint-disable-next-line no-console
|
|
254
298
|
console.warn(
|
|
255
|
-
`[Bloom] A \`renderVideo\` slot
|
|
256
|
-
'
|
|
257
|
-
'identity
|
|
258
|
-
'
|
|
299
|
+
`[Bloom] A \`renderVideo\` slot was a NEW function on ${SLOT_IDENTITY_CHURN_LIMIT} ` +
|
|
300
|
+
'consecutive renders, including ones where nothing it reads changed. The slot is ' +
|
|
301
|
+
'compared by identity — on web it is part of what a media host publishes to the ' +
|
|
302
|
+
'flight layer — so one rebuilt per render re-renders the surface per render. Wrap ' +
|
|
303
|
+
'it in `useCallback` with the props it actually reads. A slot that changes when ' +
|
|
304
|
+
'those props change is fine and does not reach this.',
|
|
259
305
|
);
|
|
260
306
|
}
|
|
261
307
|
|
|
@@ -32,16 +32,41 @@ export const SURFACE_MOUNT_TIMEOUT_MS = 250;
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* How many
|
|
35
|
+
* How many CONSECUTIVE renders a video slot may arrive new on before Bloom says
|
|
36
|
+
* so.
|
|
36
37
|
*
|
|
37
|
-
* `renderVideo` is
|
|
38
|
-
*
|
|
39
|
-
* lives in
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* `renderVideo` is compared by identity — and on web it is part of what a host
|
|
39
|
+
* publishes to the layer — so an unmemoised slot re-renders the surface on
|
|
40
|
+
* every render of the row it lives in, silently and only under load.
|
|
41
|
+
*
|
|
42
|
+
* Consecutive, never a lifetime total. A correctly memoised slot changes
|
|
43
|
+
* identity whenever the state it reads changes (`isWatched` on a reel slide,
|
|
44
|
+
* say), so ANY fixed lifetime total is reached eventually by a consumer with
|
|
45
|
+
* nothing to fix — and the warning's own advice, `useCallback` over the props
|
|
46
|
+
* the slot reads, is what such a consumer already wrote. Five in a row is
|
|
47
|
+
* unreachable that way: it takes five commits where nothing the slot reads
|
|
48
|
+
* changed and the slot was rebuilt anyway.
|
|
42
49
|
*/
|
|
43
50
|
export const SLOT_IDENTITY_CHURN_LIMIT = 5;
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* How long a run of {@link SLOT_IDENTITY_CHURN_LIMIT} new slots may take and
|
|
54
|
+
* still be reported (ms).
|
|
55
|
+
*
|
|
56
|
+
* "Consecutive" alone is not enough for the consumer who memoised the surface's
|
|
57
|
+
* other props as well: `memo` then skips every render where only the parent
|
|
58
|
+
* changed, so the surface commits ONLY when the slot changes and the run never
|
|
59
|
+
* breaks — with nothing to fix and, because the renders were skipped, no cost
|
|
60
|
+
* paid either.
|
|
61
|
+
*
|
|
62
|
+
* Rate is what separates them, and it is what the warning was always about. A
|
|
63
|
+
* slot rebuilt per render changes as fast as its row renders, which in a
|
|
64
|
+
* scrolling feed is five times in a fraction of a frame budget. A slot rebuilt
|
|
65
|
+
* when the state it reads changes moves at the speed of a finger, and half a
|
|
66
|
+
* second is far too short for five activations of ONE surface.
|
|
67
|
+
*/
|
|
68
|
+
export const SLOT_IDENTITY_CHURN_WINDOW_MS = 500;
|
|
69
|
+
|
|
45
70
|
/**
|
|
46
71
|
* How far a media element may sit from the box it is supposed to fill, in px,
|
|
47
72
|
* before Bloom says so.
|
|
@@ -276,9 +276,14 @@ export interface MediaVideoSlotProps<P extends VideoPlayerLike = VideoPlayerLike
|
|
|
276
276
|
* So the consumer may build the element instead, with Bloom deciding only what
|
|
277
277
|
* it has to: which player, and a style that fills the box.
|
|
278
278
|
*
|
|
279
|
-
* MEMOISE IT (`useCallback`)
|
|
280
|
-
*
|
|
279
|
+
* MEMOISE IT (`useCallback`), over the state the element actually reads. The
|
|
280
|
+
* slot is compared by identity — and on web it is part of what a host publishes
|
|
281
|
+
* to the layer — so a new function every render re-renders the surface every
|
|
281
282
|
* render. Correct either way, wasteful if you skip it.
|
|
283
|
+
*
|
|
284
|
+
* It is expected to change when that state does; only a slot rebuilt on renders
|
|
285
|
+
* where nothing it reads changed is the mistake, and that is the only one Bloom
|
|
286
|
+
* warns about.
|
|
282
287
|
*/
|
|
283
288
|
export type MediaVideoSlot<P extends VideoPlayerLike = VideoPlayerLike> = (
|
|
284
289
|
props: MediaVideoSlotProps<P>,
|