@oxyhq/bloom 0.48.0 → 0.49.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 +3 -1
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +30 -4
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +48 -14
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +30 -5
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/dialog/DialogHeader.js +12 -6
- package/lib/commonjs/dialog/DialogHeader.js.map +1 -1
- package/lib/commonjs/dialog/DialogMorph.js +458 -0
- package/lib/commonjs/dialog/DialogMorph.js.map +1 -0
- package/lib/commonjs/dialog/index.js +7 -0
- package/lib/commonjs/dialog/index.js.map +1 -1
- package/lib/commonjs/dialog/index.web.js +7 -0
- package/lib/commonjs/dialog/index.web.js.map +1 -1
- package/lib/commonjs/settings-list/SettingsList.js +1 -1
- package/lib/commonjs/surfaces/SurfaceHost.js +1 -0
- package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +3 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/dialog/Dialog.js +30 -4
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +48 -15
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +31 -6
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/dialog/DialogHeader.js +12 -6
- package/lib/module/dialog/DialogHeader.js.map +1 -1
- package/lib/module/dialog/DialogMorph.js +451 -0
- package/lib/module/dialog/DialogMorph.js.map +1 -0
- package/lib/module/dialog/index.js +1 -0
- package/lib/module/dialog/index.js.map +1 -1
- package/lib/module/dialog/index.web.js +1 -0
- package/lib/module/dialog/index.web.js.map +1 -1
- package/lib/module/settings-list/SettingsList.js +1 -1
- package/lib/module/surfaces/SurfaceHost.js +1 -0
- package/lib/module/surfaces/SurfaceHost.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +8 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +2 -2
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogMorph.d.ts +150 -0
- package/lib/typescript/commonjs/dialog/DialogMorph.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/index.d.ts +2 -0
- package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +30 -0
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/surfaces/types.d.ts +9 -0
- package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +8 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +2 -2
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogMorph.d.ts +150 -0
- package/lib/typescript/module/dialog/DialogMorph.d.ts.map +1 -0
- package/lib/typescript/module/dialog/index.d.ts +2 -0
- package/lib/typescript/module/dialog/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.web.d.ts +2 -0
- package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +30 -0
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/module/surfaces/types.d.ts +9 -0
- package/lib/typescript/module/surfaces/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/DialogMorph.test.tsx +230 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +13 -1
- package/src/dialog/Dialog.tsx +38 -11
- package/src/dialog/Dialog.web.tsx +40 -10
- package/src/dialog/DialogBottomSheet.tsx +35 -5
- package/src/dialog/DialogHeader.tsx +14 -5
- package/src/dialog/DialogMorph.tsx +576 -0
- package/src/dialog/index.ts +2 -0
- package/src/dialog/index.web.ts +2 -0
- package/src/dialog/types.ts +30 -0
- package/src/settings-list/SettingsList.tsx +1 -1
- package/src/surfaces/SurfaceHost.tsx +1 -0
- package/src/surfaces/types.ts +9 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SurfaceHost.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/SurfaceHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"SurfaceHost.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/SurfaceHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAgCxD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe;;;;;;uBAqGhB;YAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;SAAE;;;EAcrE"}
|
|
@@ -53,6 +53,15 @@ export interface SurfacePresentation {
|
|
|
53
53
|
* to `Dialog`.
|
|
54
54
|
*/
|
|
55
55
|
scrollable?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the surface MORPHS its panel when its content is swapped in place —
|
|
58
|
+
* navigation WITHIN this surface, as opposed to presenting a new surface on
|
|
59
|
+
* top. Defaults to `true`. The content signals the swap through
|
|
60
|
+
* `useDialogFrame`; a surface whose content never declares a frame is
|
|
61
|
+
* unaffected. Set `false` for content that must never be resized into (a
|
|
62
|
+
* full-bleed picker). Forwarded to `Dialog`.
|
|
63
|
+
*/
|
|
64
|
+
morph?: boolean;
|
|
56
65
|
/** Style overrides applied to the inner content container. */
|
|
57
66
|
style?: StyleProp<ViewStyle>;
|
|
58
67
|
/** Style overrides for the side/bottom placement panel surface. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAElG;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,mEAAmE;IACnE,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,+CAA+C;IAC/C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iEAAiE;AACjE,MAAM,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAEjD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,GAAG,OAAO,EACxB,MAAM,EAAE,eAAe,EACvB,IAAI,CAAC,EAAE,cAAc,KAClB,OAAO,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,8EAA8E;AAC9E,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,eAAe,KAAK,SAAS,CAAC;AAEtE,6CAA6C;AAC7C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,eAAe,CAAC;IACxB,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACpC,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kDAAkD;IAClD,SAAS,CAAC,EAAE,yBAAyB,CAAC;CACvC;AAED,2EAA2E;AAC3E,MAAM,WAAW,oBAAoB;IACnC,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2BAA2B;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kDAAkD;IAClD,SAAS,CAAC,EAAE,yBAAyB,CAAC;CACvC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { act, render } from '@testing-library/react-native';
|
|
3
|
+
import type { LayoutChangeEvent, ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DialogMorphContent,
|
|
7
|
+
useDialogFrame,
|
|
8
|
+
useDialogMorph,
|
|
9
|
+
} from '../dialog/DialogMorph';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Size morphing across an in-place content swap.
|
|
13
|
+
*
|
|
14
|
+
* These assert the STATE MACHINE — when the panel height is taken over and when
|
|
15
|
+
* it is handed back to natural sizing. The reanimated mock settles `withTiming`
|
|
16
|
+
* synchronously, so the frame-by-frame interpolation is NOT observable here; it
|
|
17
|
+
* is verified in a real browser, the only place reanimated's web mapper ticks.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const layout = (height: number): LayoutChangeEvent =>
|
|
21
|
+
({
|
|
22
|
+
nativeEvent: { layout: { x: 0, y: 0, width: 320, height } },
|
|
23
|
+
}) as LayoutChangeEvent;
|
|
24
|
+
|
|
25
|
+
interface HarnessProps {
|
|
26
|
+
morphEnabled?: boolean;
|
|
27
|
+
measurable?: boolean;
|
|
28
|
+
frameKey: string;
|
|
29
|
+
frameMorph?: boolean;
|
|
30
|
+
onState: (state: {
|
|
31
|
+
panelStyle: ViewStyle;
|
|
32
|
+
onPanelLayout: (e: LayoutChangeEvent) => void;
|
|
33
|
+
onContentLayout: (e: LayoutChangeEvent) => void;
|
|
34
|
+
}) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Drives one surface: measurement in, panel style + a frame-declaring child out. */
|
|
38
|
+
function Harness({
|
|
39
|
+
morphEnabled = true,
|
|
40
|
+
measurable = true,
|
|
41
|
+
frameKey,
|
|
42
|
+
frameMorph,
|
|
43
|
+
onState,
|
|
44
|
+
}: HarnessProps) {
|
|
45
|
+
const morph = useDialogMorph({ enabled: morphEnabled, measurable, maxHeight: 800 });
|
|
46
|
+
onState({
|
|
47
|
+
panelStyle: morph.panelStyle as ViewStyle,
|
|
48
|
+
onPanelLayout: morph.onPanelLayout,
|
|
49
|
+
onContentLayout: morph.onContentLayout,
|
|
50
|
+
});
|
|
51
|
+
return (
|
|
52
|
+
<DialogMorphContent morph={morph}>
|
|
53
|
+
<Frame frameKey={frameKey} frameMorph={frameMorph} />
|
|
54
|
+
</DialogMorphContent>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function Frame({ frameKey, frameMorph }: { frameKey: string; frameMorph?: boolean }) {
|
|
59
|
+
useDialogFrame({ key: frameKey, morph: frameMorph });
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Mount the harness and return a handle for driving layout + swapping frames. */
|
|
64
|
+
function mountHarness(props: Omit<Partial<HarnessProps>, 'onState' | 'frameKey'> = {}) {
|
|
65
|
+
let state: Parameters<HarnessProps['onState']>[0] | null = null;
|
|
66
|
+
const onState: HarnessProps['onState'] = (next) => {
|
|
67
|
+
state = next;
|
|
68
|
+
};
|
|
69
|
+
const element = (frameKey: string, extra: Omit<Partial<HarnessProps>, 'onState'> = {}) => (
|
|
70
|
+
<Harness frameKey={frameKey} onState={onState} {...props} {...extra} />
|
|
71
|
+
);
|
|
72
|
+
const { rerender, unmount } = render(element('a'));
|
|
73
|
+
const read = () => {
|
|
74
|
+
if (!state) throw new Error('harness did not render');
|
|
75
|
+
return state;
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
/** The panel height the surface currently resolves to. */
|
|
79
|
+
panelHeight: () => read().panelStyle.height,
|
|
80
|
+
/** Report the panel's and then the content's laid-out heights. */
|
|
81
|
+
measure(panelHeight: number, contentHeight: number) {
|
|
82
|
+
act(() => {
|
|
83
|
+
read().onPanelLayout(layout(panelHeight));
|
|
84
|
+
read().onContentLayout(layout(contentHeight));
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
/**
|
|
88
|
+
* Report only the content's natural height (the incoming frame settling).
|
|
89
|
+
* Followed by a re-render: the mock's `useAnimatedStyle` only re-evaluates
|
|
90
|
+
* on render, whereas the real one re-runs on the shared-value write.
|
|
91
|
+
*/
|
|
92
|
+
measureContent(contentHeight: number, frameKey = 'b') {
|
|
93
|
+
act(() => read().onContentLayout(layout(contentHeight)));
|
|
94
|
+
rerender(element(frameKey));
|
|
95
|
+
},
|
|
96
|
+
/** Declare a frame, then re-render so the resulting panel style is observable. */
|
|
97
|
+
swapTo(frameKey: string, extra: Omit<Partial<HarnessProps>, 'onState'> = {}) {
|
|
98
|
+
rerender(element(frameKey, extra));
|
|
99
|
+
rerender(element(frameKey, extra));
|
|
100
|
+
},
|
|
101
|
+
unmount,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe('Dialog size morphing', () => {
|
|
106
|
+
it('leaves the panel naturally sized until a frame actually changes', () => {
|
|
107
|
+
const h = mountHarness();
|
|
108
|
+
expect(h.panelHeight()).toBe('auto');
|
|
109
|
+
|
|
110
|
+
h.measure(300, 298);
|
|
111
|
+
expect(h.panelHeight()).toBe('auto');
|
|
112
|
+
|
|
113
|
+
// Re-declaring the SAME frame is not a swap.
|
|
114
|
+
h.swapTo('a');
|
|
115
|
+
expect(h.panelHeight()).toBe('auto');
|
|
116
|
+
h.unmount();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('pins the panel at the outgoing size, then releases onto the incoming one', () => {
|
|
120
|
+
const h = mountHarness();
|
|
121
|
+
h.measure(300, 298);
|
|
122
|
+
|
|
123
|
+
h.swapTo('b');
|
|
124
|
+
// Held at the size the outgoing frame was painted at — no jump while the
|
|
125
|
+
// incoming content is still being measured.
|
|
126
|
+
expect(h.panelHeight()).toBe(300);
|
|
127
|
+
|
|
128
|
+
// The incoming frame reports 500 of natural height; the panel carries the
|
|
129
|
+
// 2px of chrome calibrated from the outgoing frame, then hands the height
|
|
130
|
+
// back so later content changes size the panel normally again.
|
|
131
|
+
h.measureContent(500);
|
|
132
|
+
expect(h.panelHeight()).toBe('auto');
|
|
133
|
+
h.unmount();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('releases the pin even when the incoming frame is taller than the bound', () => {
|
|
137
|
+
const h = mountHarness();
|
|
138
|
+
h.measure(300, 300);
|
|
139
|
+
h.swapTo('b');
|
|
140
|
+
expect(h.panelHeight()).toBe(300);
|
|
141
|
+
|
|
142
|
+
h.measureContent(5000);
|
|
143
|
+
expect(h.panelHeight()).toBe('auto');
|
|
144
|
+
h.unmount();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('releases without reshaping when the incoming frame fills the space it is given', () => {
|
|
148
|
+
const h = mountHarness();
|
|
149
|
+
h.measure(300, 300);
|
|
150
|
+
h.swapTo('b');
|
|
151
|
+
expect(h.panelHeight()).toBe(300);
|
|
152
|
+
|
|
153
|
+
// A child that flexes to its container measures ~0 inside a content-sized
|
|
154
|
+
// wrapper; collapsing the panel onto that would empty the surface.
|
|
155
|
+
h.measureContent(0);
|
|
156
|
+
expect(h.panelHeight()).toBe('auto');
|
|
157
|
+
h.unmount();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('never pins when nothing has been measured yet', () => {
|
|
161
|
+
const h = mountHarness();
|
|
162
|
+
h.swapTo('b');
|
|
163
|
+
expect(h.panelHeight()).toBe('auto');
|
|
164
|
+
h.unmount();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('honours the per-frame morph opt-out', () => {
|
|
168
|
+
const h = mountHarness();
|
|
169
|
+
h.measure(300, 300);
|
|
170
|
+
h.swapTo('b', { frameMorph: false });
|
|
171
|
+
expect(h.panelHeight()).toBe('auto');
|
|
172
|
+
h.unmount();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('honours the surface-level morph opt-out', () => {
|
|
176
|
+
const h = mountHarness({ morphEnabled: false });
|
|
177
|
+
h.measure(300, 300);
|
|
178
|
+
h.swapTo('b');
|
|
179
|
+
expect(h.panelHeight()).toBe('auto');
|
|
180
|
+
h.unmount();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('does not pin a surface whose content owns its own size', () => {
|
|
184
|
+
const h = mountHarness({ measurable: false });
|
|
185
|
+
h.measure(300, 300);
|
|
186
|
+
h.swapTo('b');
|
|
187
|
+
expect(h.panelHeight()).toBe('auto');
|
|
188
|
+
h.unmount();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('drives maxWidth only on a placement that can vary it', () => {
|
|
192
|
+
let fixedWidth: ViewStyle | null = null;
|
|
193
|
+
let varyingWidth: ViewStyle | null = null;
|
|
194
|
+
render(
|
|
195
|
+
<WidthHarness
|
|
196
|
+
onStyle={(s) => {
|
|
197
|
+
fixedWidth = s;
|
|
198
|
+
}}
|
|
199
|
+
/>,
|
|
200
|
+
);
|
|
201
|
+
render(
|
|
202
|
+
<WidthHarness
|
|
203
|
+
maxWidth={480}
|
|
204
|
+
onStyle={(s) => {
|
|
205
|
+
varyingWidth = s;
|
|
206
|
+
}}
|
|
207
|
+
/>,
|
|
208
|
+
);
|
|
209
|
+
expect((fixedWidth as unknown as ViewStyle).maxWidth).toBeUndefined();
|
|
210
|
+
expect((varyingWidth as unknown as ViewStyle).maxWidth).toBe(480);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
function WidthHarness({
|
|
215
|
+
maxWidth,
|
|
216
|
+
onStyle,
|
|
217
|
+
}: {
|
|
218
|
+
maxWidth?: number;
|
|
219
|
+
onStyle: (style: ViewStyle) => void;
|
|
220
|
+
}) {
|
|
221
|
+
const morph = useDialogMorph({ enabled: true, measurable: true, maxHeight: 800, maxWidth });
|
|
222
|
+
onStyle(morph.panelStyle as ViewStyle);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
describe('useDialogFrame outside a morph-capable surface', () => {
|
|
227
|
+
it('is a no-op (a side sheet, or a plain confirm dialog)', () => {
|
|
228
|
+
expect(() => render(<Frame frameKey="only" />)).not.toThrow();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Pressable,
|
|
7
7
|
Dimensions,
|
|
8
8
|
Platform,
|
|
9
|
+
type LayoutChangeEvent,
|
|
9
10
|
type ViewStyle,
|
|
10
11
|
type StyleProp,
|
|
11
12
|
} from 'react-native';
|
|
@@ -151,6 +152,13 @@ export interface BottomSheetProps {
|
|
|
151
152
|
* Dialog to float its sticky nav header over the sheet's scroll content.
|
|
152
153
|
*/
|
|
153
154
|
headerOverlay?: React.ReactNode;
|
|
155
|
+
/**
|
|
156
|
+
* Layout callback for the SHEET CARD — the same box `style` targets. Lets a
|
|
157
|
+
* host observe the card's rendered size (the Dialog measures it to morph the
|
|
158
|
+
* sheet between two content frames). Fires on every size change, including
|
|
159
|
+
* while a host animates the card's height.
|
|
160
|
+
*/
|
|
161
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
/**
|
|
@@ -209,6 +217,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
209
217
|
handleComponent,
|
|
210
218
|
scrollY: externalScrollY,
|
|
211
219
|
headerOverlay,
|
|
220
|
+
onLayout,
|
|
212
221
|
} = props;
|
|
213
222
|
|
|
214
223
|
const insets = useSafeAreaInsets();
|
|
@@ -780,7 +789,10 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
780
789
|
</Animated.View>
|
|
781
790
|
|
|
782
791
|
<GestureDetector gesture={panGesture}>
|
|
783
|
-
<Animated.View
|
|
792
|
+
<Animated.View
|
|
793
|
+
onLayout={onLayout}
|
|
794
|
+
style={[dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle, style]}
|
|
795
|
+
>
|
|
784
796
|
{backgroundComponent?.({ style: styles.background })}
|
|
785
797
|
|
|
786
798
|
{handleSlot}
|
package/src/dialog/Dialog.tsx
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
DialogNavHeader,
|
|
39
39
|
useDialogHeaderController,
|
|
40
40
|
} from './DialogHeader';
|
|
41
|
+
import { DialogMorphContent, useDialogMorph } from './DialogMorph';
|
|
41
42
|
import {
|
|
42
43
|
ANIMATION_DURATION,
|
|
43
44
|
DEFAULT_DIALOG_CONTENT_PADDING,
|
|
@@ -113,6 +114,7 @@ function CenteredOrSideDialog({
|
|
|
113
114
|
actions,
|
|
114
115
|
header,
|
|
115
116
|
scrollable,
|
|
117
|
+
morph,
|
|
116
118
|
placement,
|
|
117
119
|
layer,
|
|
118
120
|
width = DEFAULT_SIDE_WIDTH,
|
|
@@ -131,6 +133,19 @@ function CenteredOrSideDialog({
|
|
|
131
133
|
const isSide = placement === 'left' || placement === 'right';
|
|
132
134
|
const headerController = useDialogHeaderController();
|
|
133
135
|
const scrollableResolved = scrollable ?? true;
|
|
136
|
+
const { height: viewportHeight } = useWindowDimensions();
|
|
137
|
+
|
|
138
|
+
// Size morphing across an in-place content swap. The detached centered card
|
|
139
|
+
// is content-hugging, so animating its height reshapes it about its own
|
|
140
|
+
// centre; the side placements are anchored top-to-bottom at a fixed width and
|
|
141
|
+
// have no size to morph, so they never pin (`measurable: false`). The card's
|
|
142
|
+
// own `maxHeight` (inside `BottomSheet`) does the exact capping — the viewport
|
|
143
|
+
// is just the bound the reshape may not exceed.
|
|
144
|
+
const morphState = useDialogMorph({
|
|
145
|
+
enabled: morph !== false && !isSide,
|
|
146
|
+
measurable: scrollableResolved && !isSide,
|
|
147
|
+
maxHeight: viewportHeight,
|
|
148
|
+
});
|
|
134
149
|
|
|
135
150
|
// Imperative open state for the side placement (the BottomSheet path owns its
|
|
136
151
|
// own visibility via its ref instead).
|
|
@@ -223,16 +238,21 @@ function CenteredOrSideDialog({
|
|
|
223
238
|
[close],
|
|
224
239
|
);
|
|
225
240
|
|
|
226
|
-
const sheetStyle = useMemo(
|
|
227
|
-
() =>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
241
|
+
const sheetStyle = useMemo<StyleProp<ViewStyle>>(
|
|
242
|
+
() => [
|
|
243
|
+
{
|
|
244
|
+
maxWidth: 500,
|
|
245
|
+
backgroundColor: theme.colors.background,
|
|
246
|
+
// All four corners rounded — bloom's BottomSheet defaults to top-only
|
|
247
|
+
// radius in flush mode, but we use `detached` so the whole card is
|
|
248
|
+
// floating and rounded uniformly.
|
|
249
|
+
borderRadius: 20,
|
|
250
|
+
},
|
|
251
|
+
// Drives the card's `height` only while a morph is in flight; at rest it
|
|
252
|
+
// resolves to `height: 'auto'` — the card's normal content sizing.
|
|
253
|
+
morphState.panelStyle,
|
|
254
|
+
],
|
|
255
|
+
[theme.colors.background, morphState.panelStyle],
|
|
236
256
|
);
|
|
237
257
|
|
|
238
258
|
const bodyNode = (
|
|
@@ -322,6 +342,8 @@ function CenteredOrSideDialog({
|
|
|
322
342
|
// Stronger dim when a Dialog is stacked over another sheet so the
|
|
323
343
|
// underlying sheet's handle/content doesn't bleed through.
|
|
324
344
|
backdropOpacity={0.7}
|
|
345
|
+
// Measures the floating card — the size a morph starts from.
|
|
346
|
+
onLayout={morphState.onPanelLayout}
|
|
325
347
|
style={sheetStyle}
|
|
326
348
|
>
|
|
327
349
|
<Context.Provider value={context}>
|
|
@@ -342,7 +364,12 @@ function CenteredOrSideDialog({
|
|
|
342
364
|
panelStyle,
|
|
343
365
|
]}
|
|
344
366
|
>
|
|
345
|
-
{
|
|
367
|
+
{/* The morph layer is its own node INSIDE the class-name'd container:
|
|
368
|
+
a className'd component's `onLayout` never fires on web, and this
|
|
369
|
+
one has to measure the incoming frame. */}
|
|
370
|
+
<DialogMorphContent morph={morphState}>
|
|
371
|
+
{header ? headerBody : bodyNode}
|
|
372
|
+
</DialogMorphContent>
|
|
346
373
|
</View>
|
|
347
374
|
</Context.Provider>
|
|
348
375
|
</BottomSheet>
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type StyleProp,
|
|
17
17
|
type ViewStyle,
|
|
18
18
|
} from 'react-native';
|
|
19
|
+
import Animated from 'react-native-reanimated';
|
|
19
20
|
import { RemoveScrollBar } from 'react-remove-scroll-bar';
|
|
20
21
|
|
|
21
22
|
import { Portal } from '../portal/index.web';
|
|
@@ -31,11 +32,13 @@ import {
|
|
|
31
32
|
DialogNavHeader,
|
|
32
33
|
useDialogHeaderController,
|
|
33
34
|
} from './DialogHeader';
|
|
35
|
+
import { DialogMorphContent, useDialogMorph } from './DialogMorph';
|
|
34
36
|
import {
|
|
35
37
|
ANIMATION_DURATION,
|
|
36
38
|
CENTER_FADE_OUT_DURATION,
|
|
37
39
|
DEFAULT_CENTER_MAX_WIDTH,
|
|
38
40
|
DEFAULT_DIALOG_CONTENT_PADDING,
|
|
41
|
+
DEFAULT_MAX_HEIGHT_RATIO,
|
|
39
42
|
DEFAULT_SIDE_WIDTH,
|
|
40
43
|
DIALOG_SHEET_BACKDROP_TESTID,
|
|
41
44
|
EASE_OUT,
|
|
@@ -149,6 +152,7 @@ function CenterOrSideDialog({
|
|
|
149
152
|
contentPadding = DEFAULT_DIALOG_CONTENT_PADDING,
|
|
150
153
|
maxHeightRatio,
|
|
151
154
|
scrollable,
|
|
155
|
+
morph,
|
|
152
156
|
style,
|
|
153
157
|
panelStyle,
|
|
154
158
|
panelClassName,
|
|
@@ -323,6 +327,7 @@ function CenterOrSideDialog({
|
|
|
323
327
|
contentPadding={contentPadding}
|
|
324
328
|
maxHeightRatio={maxHeightRatio}
|
|
325
329
|
scrollable={scrollable}
|
|
330
|
+
morph={morph}
|
|
326
331
|
surfaceZIndex={dialogZIndex.surface}
|
|
327
332
|
isClosing={isClosing}
|
|
328
333
|
>
|
|
@@ -383,6 +388,7 @@ function DialogPanel({
|
|
|
383
388
|
contentPadding,
|
|
384
389
|
maxHeightRatio,
|
|
385
390
|
scrollable,
|
|
391
|
+
morph,
|
|
386
392
|
surfaceZIndex,
|
|
387
393
|
isClosing,
|
|
388
394
|
children,
|
|
@@ -398,6 +404,7 @@ function DialogPanel({
|
|
|
398
404
|
contentPadding: number;
|
|
399
405
|
maxHeightRatio?: number;
|
|
400
406
|
scrollable?: boolean;
|
|
407
|
+
morph?: boolean;
|
|
401
408
|
surfaceZIndex: number;
|
|
402
409
|
isClosing: boolean;
|
|
403
410
|
children?: React.ReactNode;
|
|
@@ -407,6 +414,19 @@ function DialogPanel({
|
|
|
407
414
|
const titleId = useId();
|
|
408
415
|
const descriptionId = useId();
|
|
409
416
|
const headerController = useDialogHeaderController();
|
|
417
|
+
const { height: viewportHeight } = useWindowDimensions();
|
|
418
|
+
const heightRatio = maxHeightRatio ?? DEFAULT_MAX_HEIGHT_RATIO;
|
|
419
|
+
|
|
420
|
+
// Size morphing across an in-place content swap. The centered card is the one
|
|
421
|
+
// placement whose width can vary too, so `maxWidth` is handed over as well.
|
|
422
|
+
// The card's own `maxHeight` (the ratio percentage below) does the exact
|
|
423
|
+
// capping — the viewport is just the bound the reshape may not exceed.
|
|
424
|
+
const morphState = useDialogMorph({
|
|
425
|
+
enabled: morph !== false,
|
|
426
|
+
measurable: scrollable !== false,
|
|
427
|
+
maxHeight: viewportHeight,
|
|
428
|
+
maxWidth,
|
|
429
|
+
});
|
|
410
430
|
|
|
411
431
|
const body = (
|
|
412
432
|
<DialogBody
|
|
@@ -421,12 +441,13 @@ function DialogPanel({
|
|
|
421
441
|
);
|
|
422
442
|
|
|
423
443
|
return (
|
|
424
|
-
<View
|
|
444
|
+
<Animated.View
|
|
425
445
|
role="dialog"
|
|
426
446
|
aria-label={label}
|
|
427
447
|
aria-labelledby={title ? titleId : undefined}
|
|
428
448
|
aria-describedby={description ? descriptionId : undefined}
|
|
429
449
|
testID={testID}
|
|
450
|
+
onLayout={morphState.onPanelLayout}
|
|
430
451
|
onStartShouldSetResponder={() => true}
|
|
431
452
|
onResponderRelease={stopPropagation}
|
|
432
453
|
{...({ onClick: stopPropagation } as Record<string, unknown>)}
|
|
@@ -441,7 +462,7 @@ function DialogPanel({
|
|
|
441
462
|
// capped at `maxHeightRatio` of the viewport and scrolls inside the
|
|
442
463
|
// rounded card (via the ScrollView below) — consumers never add their
|
|
443
464
|
// own height cap / ScrollView. `overflow: hidden` clips to the radius.
|
|
444
|
-
maxHeight: `${Math.round(
|
|
465
|
+
maxHeight: `${Math.round(heightRatio * 100)}%`,
|
|
445
466
|
overflow: 'hidden',
|
|
446
467
|
backgroundColor: theme.colors.background,
|
|
447
468
|
borderWidth: 1,
|
|
@@ -455,6 +476,10 @@ function DialogPanel({
|
|
|
455
476
|
isClosing
|
|
456
477
|
? ({ animation: `bloomDialogZoomFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
|
|
457
478
|
: ({ animation: 'bloomDialogZoomFadeIn cubic-bezier(0.16, 1, 0.3, 1) 0.3s' } as ViewStyle),
|
|
479
|
+
// Drives `height` (and `maxWidth`) only while a morph is in flight; at
|
|
480
|
+
// rest it resolves to `height: 'auto'` — the natural sizing above. Placed
|
|
481
|
+
// before `style` so a consumer's explicit size still wins.
|
|
482
|
+
morphState.panelStyle,
|
|
458
483
|
style,
|
|
459
484
|
]}
|
|
460
485
|
>
|
|
@@ -471,10 +496,10 @@ function DialogPanel({
|
|
|
471
496
|
collapse={scrollable !== false}
|
|
472
497
|
/>
|
|
473
498
|
{scrollable === false ? (
|
|
474
|
-
<
|
|
499
|
+
<DialogMorphContent morph={morphState} style={{ flex: 1, minHeight: 0 }}>
|
|
475
500
|
<View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
|
|
476
501
|
<DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
|
|
477
|
-
</
|
|
502
|
+
</DialogMorphContent>
|
|
478
503
|
) : (
|
|
479
504
|
<ScrollView
|
|
480
505
|
style={{ flexShrink: 1 }}
|
|
@@ -485,8 +510,10 @@ function DialogPanel({
|
|
|
485
510
|
headerController.scrollY.value = e.nativeEvent.contentOffset.y;
|
|
486
511
|
}}
|
|
487
512
|
>
|
|
488
|
-
<
|
|
489
|
-
|
|
513
|
+
<DialogMorphContent morph={morphState}>
|
|
514
|
+
<DialogLargeTitle controller={headerController} header={header} />
|
|
515
|
+
<DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
|
|
516
|
+
</DialogMorphContent>
|
|
490
517
|
</ScrollView>
|
|
491
518
|
)}
|
|
492
519
|
</>
|
|
@@ -496,19 +523,22 @@ function DialogPanel({
|
|
|
496
523
|
// `maxHeight` + `overflow: hidden` cap it and `flex: 1` + `minHeight: 0`
|
|
497
524
|
// hand the child a bounded height to scroll into — with NO wrapping
|
|
498
525
|
// ScrollView, so a VirtualizedList never nests inside one.
|
|
499
|
-
<
|
|
526
|
+
<DialogMorphContent
|
|
527
|
+
morph={morphState}
|
|
528
|
+
style={{ flex: 1, minHeight: 0, padding: contentPadding }}
|
|
529
|
+
>
|
|
500
530
|
{body}
|
|
501
|
-
</
|
|
531
|
+
</DialogMorphContent>
|
|
502
532
|
) : (
|
|
503
533
|
<ScrollView
|
|
504
534
|
style={{ flexShrink: 1 }}
|
|
505
535
|
contentContainerStyle={{ padding: contentPadding }}
|
|
506
536
|
showsVerticalScrollIndicator={false}
|
|
507
537
|
>
|
|
508
|
-
{body}
|
|
538
|
+
<DialogMorphContent morph={morphState}>{body}</DialogMorphContent>
|
|
509
539
|
</ScrollView>
|
|
510
540
|
)}
|
|
511
|
-
</View>
|
|
541
|
+
</Animated.View>
|
|
512
542
|
);
|
|
513
543
|
}
|
|
514
544
|
|
|
@@ -6,7 +6,12 @@ import React, {
|
|
|
6
6
|
useMemo,
|
|
7
7
|
useRef,
|
|
8
8
|
} from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
useWindowDimensions,
|
|
11
|
+
View,
|
|
12
|
+
type StyleProp,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
10
15
|
|
|
11
16
|
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
12
17
|
import { useTheme } from '../theme/use-theme';
|
|
@@ -19,6 +24,7 @@ import {
|
|
|
19
24
|
DialogNavHeader,
|
|
20
25
|
useDialogHeaderController,
|
|
21
26
|
} from './DialogHeader';
|
|
27
|
+
import { DialogMorphContent, useDialogMorph } from './DialogMorph';
|
|
22
28
|
import {
|
|
23
29
|
DEFAULT_DIALOG_CONTENT_PADDING,
|
|
24
30
|
DEFAULT_MAX_HEIGHT_RATIO,
|
|
@@ -62,6 +68,7 @@ export function DialogBottomSheet({
|
|
|
62
68
|
dismissOnBackdrop = true,
|
|
63
69
|
contentPadding = DEFAULT_DIALOG_CONTENT_PADDING,
|
|
64
70
|
scrollable,
|
|
71
|
+
morph,
|
|
65
72
|
style,
|
|
66
73
|
panelStyle,
|
|
67
74
|
panelClassName,
|
|
@@ -72,6 +79,7 @@ export function DialogBottomSheet({
|
|
|
72
79
|
}: DialogBottomSheetProps) {
|
|
73
80
|
const isControlled = controlledOpen !== undefined;
|
|
74
81
|
const theme = useTheme();
|
|
82
|
+
const { height: viewportHeight } = useWindowDimensions();
|
|
75
83
|
const ref = useRef<BottomSheetRef>(null);
|
|
76
84
|
const closeCallbacks = useRef<(() => void)[]>([]);
|
|
77
85
|
const titleId = useId();
|
|
@@ -155,6 +163,19 @@ export function DialogBottomSheet({
|
|
|
155
163
|
[dismissOnBackdrop],
|
|
156
164
|
);
|
|
157
165
|
|
|
166
|
+
const scrollableResolved = scrollable ?? true;
|
|
167
|
+
|
|
168
|
+
// Size morphing across an in-place content swap. The sheet is anchored at the
|
|
169
|
+
// bottom, so animating its height makes it grow/shrink from that edge; its
|
|
170
|
+
// width is fixed by the placement. The sheet's own `maxHeight` (the ratio
|
|
171
|
+
// percentage + the safe-area clamp inside `BottomSheet`) does the exact
|
|
172
|
+
// capping — the viewport is just the bound the reshape may not exceed.
|
|
173
|
+
const morphState = useDialogMorph({
|
|
174
|
+
enabled: morph !== false,
|
|
175
|
+
measurable: scrollableResolved,
|
|
176
|
+
maxHeight: viewportHeight,
|
|
177
|
+
});
|
|
178
|
+
|
|
158
179
|
// `panelStyle` paints the sheet surface (e.g. a brand background). It composes
|
|
159
180
|
// AFTER the BottomSheet's internal background so a host palette wins; the
|
|
160
181
|
// shape (radius, max height) is supplied here and remains overridable.
|
|
@@ -168,8 +189,11 @@ export function DialogBottomSheet({
|
|
|
168
189
|
maxHeight: maxHeightPercent,
|
|
169
190
|
},
|
|
170
191
|
panelStyle,
|
|
192
|
+
// Drives the card's `height` only while a morph is in flight; at rest it
|
|
193
|
+
// resolves to `height: 'auto'` — the sheet's normal content sizing.
|
|
194
|
+
morphState.panelStyle,
|
|
171
195
|
];
|
|
172
|
-
}, [theme.colors.background, maxHeightRatio, panelStyle]);
|
|
196
|
+
}, [theme.colors.background, maxHeightRatio, panelStyle, morphState.panelStyle]);
|
|
173
197
|
|
|
174
198
|
// Does this dialog render bloom's declarative chrome (title / description /
|
|
175
199
|
// actions)? This no longer gates the SCROLL decision (that is now the explicit
|
|
@@ -183,8 +207,6 @@ export function DialogBottomSheet({
|
|
|
183
207
|
description !== undefined ||
|
|
184
208
|
(actions !== undefined && actions.length > 0);
|
|
185
209
|
|
|
186
|
-
const scrollableResolved = scrollable ?? true;
|
|
187
|
-
|
|
188
210
|
const dialogBody = (
|
|
189
211
|
<DialogBody
|
|
190
212
|
titleId={titleId}
|
|
@@ -264,6 +286,8 @@ export function DialogBottomSheet({
|
|
|
264
286
|
// Stronger dim than a lone sheet so an underlying sheet's handle/content
|
|
265
287
|
// doesn't bleed through when a Dialog is stacked over one.
|
|
266
288
|
backdropOpacity={SHEET_BACKDROP_OPACITY + 0.3}
|
|
289
|
+
// Measures the sheet card — the size a morph starts from.
|
|
290
|
+
onLayout={morphState.onPanelLayout}
|
|
267
291
|
style={sheetStyle}
|
|
268
292
|
>
|
|
269
293
|
<Context.Provider value={context}>
|
|
@@ -285,7 +309,12 @@ export function DialogBottomSheet({
|
|
|
285
309
|
panelStyle,
|
|
286
310
|
]}
|
|
287
311
|
>
|
|
288
|
-
{
|
|
312
|
+
{/* The morph layer is its own node INSIDE the class-name'd container:
|
|
313
|
+
a className'd component's `onLayout` never fires on web, and this
|
|
314
|
+
one has to measure the incoming frame. */}
|
|
315
|
+
<DialogMorphContent morph={morphState}>
|
|
316
|
+
{header ? headerBody : dialogBody}
|
|
317
|
+
</DialogMorphContent>
|
|
289
318
|
</View>
|
|
290
319
|
</Context.Provider>
|
|
291
320
|
</BottomSheet>
|
|
@@ -313,6 +342,7 @@ export type DialogBottomSheetProps = Pick<
|
|
|
313
342
|
| 'dismissOnBackdrop'
|
|
314
343
|
| 'contentPadding'
|
|
315
344
|
| 'scrollable'
|
|
345
|
+
| 'morph'
|
|
316
346
|
| 'style'
|
|
317
347
|
| 'panelStyle'
|
|
318
348
|
| 'panelClassName'
|