@oxyhq/bloom 0.48.0 → 0.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +3 -1
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  3. package/lib/commonjs/dialog/Dialog.js +35 -4
  4. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  5. package/lib/commonjs/dialog/Dialog.web.js +48 -14
  6. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  7. package/lib/commonjs/dialog/DialogBottomSheet.js +30 -5
  8. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  9. package/lib/commonjs/dialog/DialogHeader.js +12 -6
  10. package/lib/commonjs/dialog/DialogHeader.js.map +1 -1
  11. package/lib/commonjs/dialog/DialogMorph.js +458 -0
  12. package/lib/commonjs/dialog/DialogMorph.js.map +1 -0
  13. package/lib/commonjs/dialog/index.js +7 -0
  14. package/lib/commonjs/dialog/index.js.map +1 -1
  15. package/lib/commonjs/dialog/index.web.js +7 -0
  16. package/lib/commonjs/dialog/index.web.js.map +1 -1
  17. package/lib/commonjs/settings-list/SettingsList.js +1 -1
  18. package/lib/commonjs/surfaces/SurfaceHost.js +1 -0
  19. package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -1
  20. package/lib/module/bottom-sheet/BottomSheetBase.js +3 -1
  21. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  22. package/lib/module/dialog/Dialog.js +35 -4
  23. package/lib/module/dialog/Dialog.js.map +1 -1
  24. package/lib/module/dialog/Dialog.web.js +48 -15
  25. package/lib/module/dialog/Dialog.web.js.map +1 -1
  26. package/lib/module/dialog/DialogBottomSheet.js +31 -6
  27. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  28. package/lib/module/dialog/DialogHeader.js +12 -6
  29. package/lib/module/dialog/DialogHeader.js.map +1 -1
  30. package/lib/module/dialog/DialogMorph.js +451 -0
  31. package/lib/module/dialog/DialogMorph.js.map +1 -0
  32. package/lib/module/dialog/index.js +1 -0
  33. package/lib/module/dialog/index.js.map +1 -1
  34. package/lib/module/dialog/index.web.js +1 -0
  35. package/lib/module/dialog/index.web.js.map +1 -1
  36. package/lib/module/settings-list/SettingsList.js +1 -1
  37. package/lib/module/surfaces/SurfaceHost.js +1 -0
  38. package/lib/module/surfaces/SurfaceHost.js.map +1 -1
  39. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +10 -3
  40. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +2 -2
  44. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/dialog/DialogMorph.d.ts +150 -0
  47. package/lib/typescript/commonjs/dialog/DialogMorph.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/dialog/index.d.ts +2 -0
  49. package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -0
  51. package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/dialog/types.d.ts +30 -0
  53. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/surfaces/types.d.ts +9 -0
  56. package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -1
  57. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +10 -3
  58. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  59. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  60. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  61. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +2 -2
  62. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
  63. package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -1
  64. package/lib/typescript/module/dialog/DialogMorph.d.ts +150 -0
  65. package/lib/typescript/module/dialog/DialogMorph.d.ts.map +1 -0
  66. package/lib/typescript/module/dialog/index.d.ts +2 -0
  67. package/lib/typescript/module/dialog/index.d.ts.map +1 -1
  68. package/lib/typescript/module/dialog/index.web.d.ts +2 -0
  69. package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
  70. package/lib/typescript/module/dialog/types.d.ts +30 -0
  71. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  72. package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -1
  73. package/lib/typescript/module/surfaces/types.d.ts +9 -0
  74. package/lib/typescript/module/surfaces/types.d.ts.map +1 -1
  75. package/package.json +1 -1
  76. package/src/__tests__/DialogMorph.test.tsx +230 -0
  77. package/src/bottom-sheet/BottomSheetBase.tsx +15 -2
  78. package/src/dialog/Dialog.tsx +43 -11
  79. package/src/dialog/Dialog.web.tsx +40 -10
  80. package/src/dialog/DialogBottomSheet.tsx +37 -6
  81. package/src/dialog/DialogHeader.tsx +14 -5
  82. package/src/dialog/DialogMorph.tsx +576 -0
  83. package/src/dialog/index.ts +2 -0
  84. package/src/dialog/index.web.ts +2 -0
  85. package/src/dialog/types.ts +30 -0
  86. package/src/settings-list/SettingsList.tsx +1 -1
  87. package/src/surfaces/SurfaceHost.tsx +1 -0
  88. package/src/surfaces/types.ts +9 -0
@@ -17,6 +17,15 @@ export type { DialogPlacement, ResponsiveDialogPlacement } from './placement';
17
17
  export interface DialogHeaderConfig {
18
18
  /** Collapsing title — rendered large in-content and small in the nav bar. */
19
19
  title?: string;
20
+ /**
21
+ * A branded node rendered centered in the nav bar INSTEAD of the collapsing
22
+ * text title — e.g. a product wordmark. Always visible (it never collapses),
23
+ * and it suppresses the large in-content title/subtitle entirely, so the
24
+ * surface starts flush under the bar. `title`/`subtitle` are ignored while
25
+ * this is set. Must be referentially stable (memoize it) so the header does
26
+ * not thrash.
27
+ */
28
+ titleContent?: ReactNode;
20
29
  /** Supporting copy under the large title (and, optionally, in the bar). */
21
30
  subtitle?: string;
22
31
  /**
@@ -214,6 +223,27 @@ export type DialogProps = React.PropsWithChildren<{
214
223
  contentPadding?: number;
215
224
  /** Whether tapping the backdrop dismisses the dialog. Defaults to `true`. */
216
225
  dismissOnBackdrop?: boolean;
226
+ /**
227
+ * Whether the panel MORPHS across an in-place content swap. Defaults to
228
+ * `true`.
229
+ *
230
+ * When the content inside an OPEN dialog is replaced (a nav-within drill-in,
231
+ * as opposed to presenting a new surface on top), the panel animates from the
232
+ * outgoing content's size to the incoming one's while the incoming content
233
+ * fades in, instead of hard-cutting. The swap is signalled by the content
234
+ * itself through `useDialogFrame` — a dialog whose content never declares a
235
+ * frame is unaffected either way.
236
+ *
237
+ * The centered card morphs its height (about its centre) and its `maxWidth`;
238
+ * the bottom sheet morphs its height from its anchored edge; side sheets have
239
+ * fixed geometry and never morph. Morphing also requires the Dialog to own the
240
+ * scroll container: a `scrollable={false}` surface owns its own size, so its
241
+ * content only cross-fades.
242
+ *
243
+ * Set `false` for content that must never be resized into — e.g. a full-bleed
244
+ * picker that always fills the surface.
245
+ */
246
+ morph?: boolean;
217
247
  /**
218
248
  * Style overrides applied to the inner content container on native (the
219
249
  * floating bottom-sheet card) and to the modal panel on web.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/dialog/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/dialog/types.ts"],"names":[],"mappings":"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"}
@@ -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;AA+BxD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe;;;;;;uBAqGhB;YAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;SAAE;;;EAcrE"}
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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.48.0",
3
+ "version": "0.49.1",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -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,12 +6,14 @@ 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';
12
13
  import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler';
13
14
  import { Z_INDEX } from '../styles/z-index';
14
15
  import Animated, {
16
+ type AnimatedStyle,
15
17
  interpolate,
16
18
  runOnJS,
17
19
  type SharedValue,
@@ -75,7 +77,7 @@ export interface BottomSheetProps {
75
77
  * background color, border radius, etc. Composed AFTER the internal sheet
76
78
  * styles so it can override them.
77
79
  */
78
- style?: StyleProp<ViewStyle>;
80
+ style?: StyleProp<AnimatedStyle<ViewStyle>>;
79
81
  enableHandlePanningGesture?: boolean;
80
82
  onDismissAttempt?: () => boolean;
81
83
  detached?: boolean; // If true, shows with margins and rounded corners. If false, full width with rounded top only.
@@ -151,6 +153,13 @@ export interface BottomSheetProps {
151
153
  * Dialog to float its sticky nav header over the sheet's scroll content.
152
154
  */
153
155
  headerOverlay?: React.ReactNode;
156
+ /**
157
+ * Layout callback for the SHEET CARD — the same box `style` targets. Lets a
158
+ * host observe the card's rendered size (the Dialog measures it to morph the
159
+ * sheet between two content frames). Fires on every size change, including
160
+ * while a host animates the card's height.
161
+ */
162
+ onLayout?: (event: LayoutChangeEvent) => void;
154
163
  }
155
164
 
156
165
  /**
@@ -209,6 +218,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
209
218
  handleComponent,
210
219
  scrollY: externalScrollY,
211
220
  headerOverlay,
221
+ onLayout,
212
222
  } = props;
213
223
 
214
224
  const insets = useSafeAreaInsets();
@@ -780,7 +790,10 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
780
790
  </Animated.View>
781
791
 
782
792
  <GestureDetector gesture={panGesture}>
783
- <Animated.View style={[dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle, style]}>
793
+ <Animated.View
794
+ onLayout={onLayout}
795
+ style={[dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle, style]}
796
+ >
784
797
  {backgroundComponent?.({ style: styles.background })}
785
798
 
786
799
  {handleSlot}
@@ -16,6 +16,7 @@ import {
16
16
  type ViewStyle,
17
17
  } from 'react-native';
18
18
  import Animated, {
19
+ type AnimatedStyle,
19
20
  cancelAnimation,
20
21
  Easing,
21
22
  runOnJS,
@@ -38,6 +39,7 @@ import {
38
39
  DialogNavHeader,
39
40
  useDialogHeaderController,
40
41
  } from './DialogHeader';
42
+ import { DialogMorphContent, useDialogMorph } from './DialogMorph';
41
43
  import {
42
44
  ANIMATION_DURATION,
43
45
  DEFAULT_DIALOG_CONTENT_PADDING,
@@ -113,6 +115,7 @@ function CenteredOrSideDialog({
113
115
  actions,
114
116
  header,
115
117
  scrollable,
118
+ morph,
116
119
  placement,
117
120
  layer,
118
121
  width = DEFAULT_SIDE_WIDTH,
@@ -131,6 +134,19 @@ function CenteredOrSideDialog({
131
134
  const isSide = placement === 'left' || placement === 'right';
132
135
  const headerController = useDialogHeaderController();
133
136
  const scrollableResolved = scrollable ?? true;
137
+ const { height: viewportHeight } = useWindowDimensions();
138
+
139
+ // Size morphing across an in-place content swap. The detached centered card
140
+ // is content-hugging, so animating its height reshapes it about its own
141
+ // centre; the side placements are anchored top-to-bottom at a fixed width and
142
+ // have no size to morph, so they never pin (`measurable: false`). The card's
143
+ // own `maxHeight` (inside `BottomSheet`) does the exact capping — the viewport
144
+ // is just the bound the reshape may not exceed.
145
+ const morphState = useDialogMorph({
146
+ enabled: morph !== false && !isSide,
147
+ measurable: scrollableResolved && !isSide,
148
+ maxHeight: viewportHeight,
149
+ });
134
150
 
135
151
  // Imperative open state for the side placement (the BottomSheet path owns its
136
152
  // own visibility via its ref instead).
@@ -223,16 +239,25 @@ function CenteredOrSideDialog({
223
239
  [close],
224
240
  );
225
241
 
226
- const sheetStyle = useMemo(
227
- () => ({
228
- maxWidth: 500,
229
- backgroundColor: theme.colors.background,
230
- // All four corners rounded — bloom's BottomSheet defaults to top-only
231
- // radius in flush mode, but we use `detached` so the whole card is
232
- // floating and rounded uniformly.
233
- borderRadius: 20,
234
- }),
235
- [theme.colors.background],
242
+ // The array composes the plain surface styles with the morph's reanimated
243
+ // animated style (`morphState.panelStyle` from `useAnimatedStyle`), so the
244
+ // element type must admit an animated style, not just a plain `ViewStyle`.
245
+ // It flows to `<BottomSheet style>`, which applies it to its outer Animated.View.
246
+ const sheetStyle = useMemo<StyleProp<AnimatedStyle<ViewStyle>>>(
247
+ () => [
248
+ {
249
+ maxWidth: 500,
250
+ backgroundColor: theme.colors.background,
251
+ // All four corners rounded — bloom's BottomSheet defaults to top-only
252
+ // radius in flush mode, but we use `detached` so the whole card is
253
+ // floating and rounded uniformly.
254
+ borderRadius: 20,
255
+ },
256
+ // Drives the card's `height` only while a morph is in flight; at rest it
257
+ // resolves to `height: 'auto'` — the card's normal content sizing.
258
+ morphState.panelStyle,
259
+ ],
260
+ [theme.colors.background, morphState.panelStyle],
236
261
  );
237
262
 
238
263
  const bodyNode = (
@@ -322,6 +347,8 @@ function CenteredOrSideDialog({
322
347
  // Stronger dim when a Dialog is stacked over another sheet so the
323
348
  // underlying sheet's handle/content doesn't bleed through.
324
349
  backdropOpacity={0.7}
350
+ // Measures the floating card — the size a morph starts from.
351
+ onLayout={morphState.onPanelLayout}
325
352
  style={sheetStyle}
326
353
  >
327
354
  <Context.Provider value={context}>
@@ -342,7 +369,12 @@ function CenteredOrSideDialog({
342
369
  panelStyle,
343
370
  ]}
344
371
  >
345
- {header ? headerBody : bodyNode}
372
+ {/* The morph layer is its own node INSIDE the class-name'd container:
373
+ a className'd component's `onLayout` never fires on web, and this
374
+ one has to measure the incoming frame. */}
375
+ <DialogMorphContent morph={morphState}>
376
+ {header ? headerBody : bodyNode}
377
+ </DialogMorphContent>
346
378
  </View>
347
379
  </Context.Provider>
348
380
  </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((maxHeightRatio ?? 0.9) * 100)}%`,
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
- <View style={{ flex: 1, minHeight: 0 }}>
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
- </View>
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
- <DialogLargeTitle controller={headerController} header={header} />
489
- <DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
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
- <View style={{ flex: 1, minHeight: 0, padding: contentPadding }}>
526
+ <DialogMorphContent
527
+ morph={morphState}
528
+ style={{ flex: 1, minHeight: 0, padding: contentPadding }}
529
+ >
500
530
  {body}
501
- </View>
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