@oxyhq/bloom 0.45.0 → 0.48.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 +34 -12
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/combobox/Combobox.js +3 -3
- package/lib/commonjs/combobox/Combobox.js.map +1 -1
- package/lib/commonjs/command/Command.js +3 -3
- package/lib/commonjs/command/Command.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +86 -8
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +176 -18
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +84 -29
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/dialog/DialogHeader.js +365 -0
- package/lib/commonjs/dialog/DialogHeader.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/index.js +40 -4
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +84 -48
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/react-native-className.d.js +6 -0
- package/lib/commonjs/react-native-className.d.js.map +1 -0
- package/lib/commonjs/{search-input → search}/index.js +5 -3
- package/lib/commonjs/search/index.js.map +1 -0
- package/lib/commonjs/settings-list/SettingsList.js +5 -1
- package/lib/commonjs/settings-list/SettingsList.js.map +1 -1
- package/lib/commonjs/surfaces/SurfaceHost.js +202 -0
- package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -0
- package/lib/commonjs/surfaces/api.js +83 -0
- package/lib/commonjs/surfaces/api.js.map +1 -0
- package/lib/commonjs/surfaces/index.js +82 -0
- package/lib/commonjs/surfaces/index.js.map +1 -0
- package/lib/commonjs/surfaces/index.web.js +77 -0
- package/lib/commonjs/surfaces/index.web.js.map +1 -0
- package/lib/commonjs/surfaces/prompts.js +154 -0
- package/lib/commonjs/surfaces/prompts.js.map +1 -0
- package/lib/commonjs/surfaces/surfaceStore.js +172 -0
- package/lib/commonjs/surfaces/surfaceStore.js.map +1 -0
- package/lib/commonjs/surfaces/types.js +6 -0
- package/lib/commonjs/surfaces/types.js.map +1 -0
- package/lib/commonjs/surfaces/useSurface.js +36 -0
- package/lib/commonjs/surfaces/useSurface.js.map +1 -0
- package/lib/commonjs/text-field/index.js +22 -6
- package/lib/commonjs/text-field/index.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +34 -12
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/combobox/Combobox.js +3 -3
- package/lib/module/combobox/Combobox.js.map +1 -1
- package/lib/module/command/Command.js +3 -3
- package/lib/module/command/Command.js.map +1 -1
- package/lib/module/dialog/Dialog.js +88 -10
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +179 -21
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +85 -30
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/dialog/DialogHeader.js +356 -0
- package/lib/module/dialog/DialogHeader.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/index.js +7 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +7 -2
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/react-native-className.d.js +20 -0
- package/lib/module/react-native-className.d.js.map +1 -0
- package/lib/module/{search-input → search}/index.js +6 -2
- package/lib/module/search/index.js.map +1 -0
- package/lib/module/settings-list/SettingsList.js +5 -1
- package/lib/module/settings-list/SettingsList.js.map +1 -1
- package/lib/module/surfaces/SurfaceHost.js +197 -0
- package/lib/module/surfaces/SurfaceHost.js.map +1 -0
- package/lib/module/surfaces/api.js +48 -0
- package/lib/module/surfaces/api.js.map +1 -0
- package/lib/module/surfaces/index.js +24 -0
- package/lib/module/surfaces/index.js.map +1 -0
- package/lib/module/surfaces/index.web.js +19 -0
- package/lib/module/surfaces/index.web.js.map +1 -0
- package/lib/module/surfaces/prompts.js +148 -0
- package/lib/module/surfaces/prompts.js.map +1 -0
- package/lib/module/surfaces/surfaceStore.js +159 -0
- package/lib/module/surfaces/surfaceStore.js.map +1 -0
- package/lib/module/surfaces/types.js +4 -0
- package/lib/module/surfaces/types.js.map +1 -0
- package/lib/module/surfaces/useSurface.js +31 -0
- package/lib/module/surfaces/useSurface.js.map +1 -0
- package/lib/module/text-field/index.js +22 -6
- package/lib/module/text-field/index.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +23 -0
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/combobox/Combobox.d.ts +1 -1
- package/lib/typescript/commonjs/command/Command.d.ts +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 +86 -0
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/index.d.ts +2 -1
- package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +85 -0
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/common.d.ts +1 -0
- package/lib/typescript/commonjs/icons/common.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +5 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +5 -3
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/input-group/types.d.ts +1 -1
- package/lib/typescript/commonjs/{search-input → search}/index.d.ts +1 -1
- package/lib/typescript/commonjs/search/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts +25 -0
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/api.d.ts +41 -0
- package/lib/typescript/commonjs/surfaces/api.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/index.d.ts +25 -0
- package/lib/typescript/commonjs/surfaces/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/index.web.d.ts +20 -0
- package/lib/typescript/commonjs/surfaces/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/prompts.d.ts +13 -0
- package/lib/typescript/commonjs/surfaces/prompts.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/surfaceStore.d.ts +51 -0
- package/lib/typescript/commonjs/surfaces/surfaceStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/types.d.ts +161 -0
- package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/useSurface.d.ts +21 -0
- package/lib/typescript/commonjs/surfaces/useSurface.d.ts.map +1 -0
- package/lib/typescript/commonjs/text-field/index.d.ts +2 -1
- package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +23 -0
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/combobox/Combobox.d.ts +1 -1
- package/lib/typescript/module/command/Command.d.ts +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 +86 -0
- package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -0
- package/lib/typescript/module/dialog/index.d.ts +2 -1
- package/lib/typescript/module/dialog/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.web.d.ts +2 -1
- package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +85 -0
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/icons/common.d.ts +1 -0
- package/lib/typescript/module/icons/common.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +5 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +5 -3
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/input-group/types.d.ts +1 -1
- package/lib/typescript/module/{search-input → search}/index.d.ts +1 -1
- package/lib/typescript/module/search/index.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts +25 -0
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/api.d.ts +41 -0
- package/lib/typescript/module/surfaces/api.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/index.d.ts +25 -0
- package/lib/typescript/module/surfaces/index.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/index.web.d.ts +20 -0
- package/lib/typescript/module/surfaces/index.web.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/prompts.d.ts +13 -0
- package/lib/typescript/module/surfaces/prompts.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/surfaceStore.d.ts +51 -0
- package/lib/typescript/module/surfaces/surfaceStore.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/types.d.ts +161 -0
- package/lib/typescript/module/surfaces/types.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/useSurface.d.ts +21 -0
- package/lib/typescript/module/surfaces/useSurface.d.ts.map +1 -0
- package/lib/typescript/module/text-field/index.d.ts +2 -1
- package/lib/typescript/module/text-field/index.d.ts.map +1 -1
- package/package.json +24 -8
- package/src/__tests__/BottomSheet.test.tsx +47 -0
- package/src/__tests__/DialogBottomSheet.test.tsx +30 -5
- package/src/__tests__/surfaceStore.test.ts +219 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +59 -10
- package/src/combobox/Combobox.tsx +3 -3
- package/src/command/Command.tsx +3 -3
- package/src/dialog/Dialog.tsx +88 -7
- package/src/dialog/Dialog.web.tsx +179 -29
- package/src/dialog/DialogBottomSheet.tsx +90 -28
- package/src/dialog/DialogHeader.tsx +420 -0
- package/src/dialog/index.ts +2 -0
- package/src/dialog/index.web.ts +2 -0
- package/src/dialog/types.ts +86 -0
- package/src/index.ts +18 -1
- package/src/index.web.ts +18 -1
- package/src/input-group/types.ts +1 -1
- package/src/react-native-className.d.ts +38 -0
- package/src/{search-input → search}/index.tsx +8 -5
- package/src/settings-list/SettingsList.tsx +5 -1
- package/src/surfaces/SurfaceHost.tsx +222 -0
- package/src/surfaces/api.ts +55 -0
- package/src/surfaces/index.ts +38 -0
- package/src/surfaces/index.web.ts +33 -0
- package/src/surfaces/prompts.tsx +178 -0
- package/src/surfaces/surfaceStore.ts +168 -0
- package/src/surfaces/types.ts +172 -0
- package/src/surfaces/useSurface.ts +33 -0
- package/src/text-field/index.tsx +24 -5
- package/lib/commonjs/search-input/index.js.map +0 -1
- package/lib/module/search-input/index.js.map +0 -1
- package/lib/typescript/commonjs/search-input/index.d.ts.map +0 -1
- package/lib/typescript/module/search-input/index.d.ts.map +0 -1
|
@@ -248,6 +248,53 @@ describe('BottomSheet', () => {
|
|
|
248
248
|
});
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
describe('unmount-while-open does not fire onDismiss (responsive placement swap)', () => {
|
|
252
|
+
it('skips onDismiss when a fully-open sheet is unmounted (yanked, not closing)', () => {
|
|
253
|
+
// A responsive Dialog crossing its breakpoint swaps its bottom-sheet branch
|
|
254
|
+
// for the centered-card branch, unmounting this sheet while it is still
|
|
255
|
+
// fully OPEN. That is NOT a dismissal — firing onDismiss here would tear the
|
|
256
|
+
// surface down mid-swap. The unmount-safety only flushes when a close was
|
|
257
|
+
// already underway (visible=false).
|
|
258
|
+
const onDismiss = jest.fn();
|
|
259
|
+
const ref = createRef<BottomSheetRef>();
|
|
260
|
+
const { unmount } = renderWithTheme(
|
|
261
|
+
<BottomSheet ref={ref} onDismiss={onDismiss}>
|
|
262
|
+
<React.Fragment>Open content</React.Fragment>
|
|
263
|
+
</BottomSheet>,
|
|
264
|
+
);
|
|
265
|
+
act(() => {
|
|
266
|
+
ref.current?.present();
|
|
267
|
+
});
|
|
268
|
+
act(() => {
|
|
269
|
+
unmount();
|
|
270
|
+
});
|
|
271
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('still flushes onDismiss when unmounted mid-close (visible already false)', () => {
|
|
275
|
+
// The original guard's purpose: a sheet yanked mid-close-animation must
|
|
276
|
+
// still flush its onDismiss so queued post-close callbacks are not lost.
|
|
277
|
+
const onDismiss = jest.fn();
|
|
278
|
+
const ref = createRef<BottomSheetRef>();
|
|
279
|
+
const { unmount } = renderWithTheme(
|
|
280
|
+
<BottomSheet ref={ref} onDismiss={onDismiss}>
|
|
281
|
+
<React.Fragment>Closing content</React.Fragment>
|
|
282
|
+
</BottomSheet>,
|
|
283
|
+
);
|
|
284
|
+
act(() => {
|
|
285
|
+
ref.current?.present();
|
|
286
|
+
});
|
|
287
|
+
// Start the close (visible → false), then yank before the exit settles.
|
|
288
|
+
act(() => {
|
|
289
|
+
ref.current?.dismiss();
|
|
290
|
+
});
|
|
291
|
+
act(() => {
|
|
292
|
+
unmount();
|
|
293
|
+
});
|
|
294
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
251
298
|
describe('close generation tracking', () => {
|
|
252
299
|
it('re-opening the sheet after dismiss does not throw or get stuck', () => {
|
|
253
300
|
// Regression test for the "tap to open does nothing" bug: when the
|
|
@@ -206,7 +206,7 @@ describe('Dialog bottom placement delegates to BottomSheet', () => {
|
|
|
206
206
|
expect(flattened.padding).toBe(0);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
it('
|
|
209
|
+
it('wraps pure custom children in the internal scroll container by default (Dialog owns scroll)', () => {
|
|
210
210
|
let control: ReturnType<typeof useDialogControl> | undefined;
|
|
211
211
|
const result = renderWithTheme(
|
|
212
212
|
<Harness>
|
|
@@ -214,7 +214,7 @@ describe('Dialog bottom placement delegates to BottomSheet', () => {
|
|
|
214
214
|
control = c;
|
|
215
215
|
return (
|
|
216
216
|
<Dialog control={c} placement="bottom">
|
|
217
|
-
<Text>Custom children
|
|
217
|
+
<Text>Custom children scroll inside the sheet by default</Text>
|
|
218
218
|
</Dialog>
|
|
219
219
|
);
|
|
220
220
|
}}
|
|
@@ -223,9 +223,34 @@ describe('Dialog bottom placement delegates to BottomSheet', () => {
|
|
|
223
223
|
act(() => {
|
|
224
224
|
control?.open();
|
|
225
225
|
});
|
|
226
|
-
// Pure custom children
|
|
227
|
-
//
|
|
228
|
-
//
|
|
226
|
+
// Pure custom children default to `scrollable` (undefined → true), so the
|
|
227
|
+
// sheet DOES add its internal ScrollView and the screen — stripped of its
|
|
228
|
+
// own scroller — scrolls inside it. The Dialog owns the scroll boundary.
|
|
229
|
+
const tree: RenderedNode | RenderedNode[] | null = result.toJSON();
|
|
230
|
+
expect(countNodesByType(tree, 'Animated.ScrollView')).toBe(1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('opts custom children OUT of the internal scroll container when scrollable={false}', () => {
|
|
234
|
+
let control: ReturnType<typeof useDialogControl> | undefined;
|
|
235
|
+
const result = renderWithTheme(
|
|
236
|
+
<Harness>
|
|
237
|
+
{(c) => {
|
|
238
|
+
control = c;
|
|
239
|
+
return (
|
|
240
|
+
<Dialog control={c} placement="bottom" scrollable={false}>
|
|
241
|
+
<Text>Owns its own FlatList</Text>
|
|
242
|
+
</Dialog>
|
|
243
|
+
);
|
|
244
|
+
}}
|
|
245
|
+
</Harness>,
|
|
246
|
+
);
|
|
247
|
+
act(() => {
|
|
248
|
+
control?.open();
|
|
249
|
+
});
|
|
250
|
+
// A surface whose content owns its own scroller (a FlatList / VirtualizedList)
|
|
251
|
+
// passes `scrollable={false}`, so the sheet does NOT add its internal
|
|
252
|
+
// ScrollView — the child is then the ONLY scroll container (no scroll-in-
|
|
253
|
+
// scroll, no "VirtualizedList inside a plain ScrollView" warning).
|
|
229
254
|
const tree: RenderedNode | RenderedNode[] | null = result.toJSON();
|
|
230
255
|
expect(countNodesByType(tree, 'Animated.ScrollView')).toBe(0);
|
|
231
256
|
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__resetSurfacesForTests,
|
|
3
|
+
dismissAll,
|
|
4
|
+
dismissToRoot,
|
|
5
|
+
dismissTop,
|
|
6
|
+
finalizeClose,
|
|
7
|
+
getSnapshot,
|
|
8
|
+
present,
|
|
9
|
+
requestDismiss,
|
|
10
|
+
resetSurfaces,
|
|
11
|
+
subscribe,
|
|
12
|
+
} from '../surfaces/surfaceStore';
|
|
13
|
+
import { confirm, prompt, surfaces } from '../surfaces/api';
|
|
14
|
+
|
|
15
|
+
const noop = () => null;
|
|
16
|
+
|
|
17
|
+
/** Id of the current top surface (guarded so strict tsc is satisfied). */
|
|
18
|
+
function topId(): string {
|
|
19
|
+
const stack = getSnapshot();
|
|
20
|
+
const top = stack[stack.length - 1];
|
|
21
|
+
if (!top) throw new Error('expected a surface on the stack');
|
|
22
|
+
return top.id;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Resolve any lingering promises so awaiting tests never hang, then reset the
|
|
27
|
+
// module store + id counter for isolation.
|
|
28
|
+
resetSurfaces();
|
|
29
|
+
__resetSurfacesForTests();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('surfaceStore', () => {
|
|
33
|
+
it('present pushes an entry and resolves its promise on dismiss', async () => {
|
|
34
|
+
const p = present(noop);
|
|
35
|
+
const stack = getSnapshot();
|
|
36
|
+
expect(stack).toHaveLength(1);
|
|
37
|
+
expect(stack[0]?.status).toBe('open');
|
|
38
|
+
requestDismiss(topId(), 'hello');
|
|
39
|
+
await expect(p).resolves.toBe('hello');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('stacks bottom→top; the last entry is the top', () => {
|
|
43
|
+
void present(noop);
|
|
44
|
+
void present(noop);
|
|
45
|
+
void present(noop);
|
|
46
|
+
expect(getSnapshot().map((e) => e.id)).toEqual([
|
|
47
|
+
'bloom-surface-1',
|
|
48
|
+
'bloom-surface-2',
|
|
49
|
+
'bloom-surface-3',
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('resolves ONCE — a second dismiss with a different result is ignored', async () => {
|
|
54
|
+
const p = present(noop);
|
|
55
|
+
const id = topId();
|
|
56
|
+
requestDismiss(id, 'first');
|
|
57
|
+
requestDismiss(id, 'second');
|
|
58
|
+
await expect(p).resolves.toBe('first');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('requestDismiss flips the entry to closing but does not splice it', () => {
|
|
62
|
+
void present(noop);
|
|
63
|
+
const id = topId();
|
|
64
|
+
requestDismiss(id, 1);
|
|
65
|
+
const stack = getSnapshot();
|
|
66
|
+
expect(stack).toHaveLength(1);
|
|
67
|
+
expect(stack[0]?.status).toBe('closing');
|
|
68
|
+
expect(stack[0]?.generation).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('finalizeClose removes a CLOSING entry; is a no-op for an OPEN entry', async () => {
|
|
72
|
+
const p = present(noop);
|
|
73
|
+
const id = topId();
|
|
74
|
+
// Open → no-op.
|
|
75
|
+
finalizeClose(id);
|
|
76
|
+
expect(getSnapshot()).toHaveLength(1);
|
|
77
|
+
// Closing → removed.
|
|
78
|
+
requestDismiss(id, 'x');
|
|
79
|
+
finalizeClose(id);
|
|
80
|
+
expect(getSnapshot()).toHaveLength(0);
|
|
81
|
+
await expect(p).resolves.toBe('x');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('dismissTop dismisses only the top surface', async () => {
|
|
85
|
+
const a = present(noop);
|
|
86
|
+
const b = present(noop);
|
|
87
|
+
dismissTop('B');
|
|
88
|
+
await expect(b).resolves.toBe('B');
|
|
89
|
+
const stack = getSnapshot();
|
|
90
|
+
expect(stack.find((e) => e.id === 'bloom-surface-1')?.status).toBe('open');
|
|
91
|
+
expect(stack.find((e) => e.id === 'bloom-surface-2')?.status).toBe('closing');
|
|
92
|
+
// Clean up the still-open root.
|
|
93
|
+
void a;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('dismissToRoot resolves everything above the root with undefined, keeps the root', async () => {
|
|
97
|
+
const root = present(noop);
|
|
98
|
+
const mid = present(noop);
|
|
99
|
+
const top = present(noop);
|
|
100
|
+
dismissToRoot();
|
|
101
|
+
await expect(mid).resolves.toBeUndefined();
|
|
102
|
+
await expect(top).resolves.toBeUndefined();
|
|
103
|
+
const stack = getSnapshot();
|
|
104
|
+
expect(stack.find((e) => e.id === 'bloom-surface-1')?.status).toBe('open');
|
|
105
|
+
void root;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('dismissToRoot is a no-op with 0 or 1 surfaces', async () => {
|
|
109
|
+
dismissToRoot();
|
|
110
|
+
expect(getSnapshot()).toHaveLength(0);
|
|
111
|
+
const only = present(noop);
|
|
112
|
+
dismissToRoot();
|
|
113
|
+
expect(getSnapshot()[0]?.status).toBe('open');
|
|
114
|
+
void only;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('dismissAll resolves ALL pending awaiters with undefined', async () => {
|
|
118
|
+
const a = present(noop);
|
|
119
|
+
const b = present(noop);
|
|
120
|
+
const c = present(noop);
|
|
121
|
+
dismissAll();
|
|
122
|
+
await expect(a).resolves.toBeUndefined();
|
|
123
|
+
await expect(b).resolves.toBeUndefined();
|
|
124
|
+
await expect(c).resolves.toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('resetSurfaces resolves pending awaiters and clears the stack', async () => {
|
|
128
|
+
const a = present(noop);
|
|
129
|
+
const b = present(noop);
|
|
130
|
+
resetSurfaces();
|
|
131
|
+
await expect(a).resolves.toBeUndefined();
|
|
132
|
+
await expect(b).resolves.toBeUndefined();
|
|
133
|
+
expect(getSnapshot()).toHaveLength(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('requestDismiss / finalizeClose ignore an unknown id', () => {
|
|
137
|
+
expect(() => requestDismiss('does-not-exist', 1)).not.toThrow();
|
|
138
|
+
expect(() => finalizeClose('does-not-exist')).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('getSnapshot returns a STABLE reference while unchanged', () => {
|
|
142
|
+
const s1 = getSnapshot();
|
|
143
|
+
expect(getSnapshot()).toBe(s1);
|
|
144
|
+
void present(noop);
|
|
145
|
+
const s2 = getSnapshot();
|
|
146
|
+
expect(s2).not.toBe(s1);
|
|
147
|
+
expect(getSnapshot()).toBe(s2);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('notifies subscribers on each stack mutation', () => {
|
|
151
|
+
let calls = 0;
|
|
152
|
+
const unsub = subscribe(() => {
|
|
153
|
+
calls += 1;
|
|
154
|
+
});
|
|
155
|
+
void present(noop); // +1
|
|
156
|
+
dismissTop(); // requestDismiss emits +1
|
|
157
|
+
expect(calls).toBe(2);
|
|
158
|
+
unsub();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('surfaces imperative API', () => {
|
|
163
|
+
it('surfaces.present + surfaces.dismiss resolve with the top result', async () => {
|
|
164
|
+
const p = surfaces.present(noop);
|
|
165
|
+
surfaces.dismiss('done');
|
|
166
|
+
await expect(p).resolves.toBe('done');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('surfaces.dismissById targets a specific surface', async () => {
|
|
170
|
+
const a = surfaces.present(noop);
|
|
171
|
+
const b = surfaces.present(noop);
|
|
172
|
+
const bottom = getSnapshot()[0];
|
|
173
|
+
if (!bottom) throw new Error('expected a surface on the stack');
|
|
174
|
+
surfaces.dismissById(bottom.id, 'A');
|
|
175
|
+
await expect(a).resolves.toBe('A');
|
|
176
|
+
void b;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('surfaces.dismissAll clears the stack and resolves pending', async () => {
|
|
180
|
+
const a = surfaces.present(noop);
|
|
181
|
+
const b = surfaces.present(noop);
|
|
182
|
+
surfaces.dismissAll();
|
|
183
|
+
await expect(a).resolves.toBeUndefined();
|
|
184
|
+
await expect(b).resolves.toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('confirm / prompt surfaces', () => {
|
|
189
|
+
it('confirm resolves true when dismissed with true, false otherwise', async () => {
|
|
190
|
+
const yes = confirm({ title: 'Delete?' });
|
|
191
|
+
requestDismiss(topId(), true);
|
|
192
|
+
await expect(yes).resolves.toBe(true);
|
|
193
|
+
|
|
194
|
+
const no = confirm({ title: 'Delete?' });
|
|
195
|
+
requestDismiss(topId(), false);
|
|
196
|
+
await expect(no).resolves.toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('confirm resolves false when dismissed without a result (backdrop/Escape)', async () => {
|
|
200
|
+
const c = confirm({ title: 'Proceed?' });
|
|
201
|
+
dismissTop(); // undefined → coerced to false
|
|
202
|
+
await expect(c).resolves.toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('confirm presents a centered surface', () => {
|
|
206
|
+
void confirm({ title: 'Hi' });
|
|
207
|
+
expect(getSnapshot()[0]?.presentation.placement).toBe('center');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('prompt resolves the entered string, or null when dismissed without one', async () => {
|
|
211
|
+
const p = prompt({ title: 'Name?' });
|
|
212
|
+
requestDismiss(topId(), 'Ada');
|
|
213
|
+
await expect(p).resolves.toBe('Ada');
|
|
214
|
+
|
|
215
|
+
const cancelled = prompt({ title: 'Name?' });
|
|
216
|
+
dismissTop(); // undefined → coerced to null
|
|
217
|
+
await expect(cancelled).resolves.toBeNull();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -55,6 +55,16 @@ export interface BottomSheetRef {
|
|
|
55
55
|
|
|
56
56
|
export interface BottomSheetProps {
|
|
57
57
|
children: React.ReactNode;
|
|
58
|
+
/**
|
|
59
|
+
* Controlled open state. When provided, the sheet mounts already-open if
|
|
60
|
+
* `true` (seeding its internal visible/rendered state) instead of relying on
|
|
61
|
+
* an imperative `present()` after mount. This is what makes a responsive
|
|
62
|
+
* `Dialog` survive a placement swap (centered card ↔ bottom sheet on resize):
|
|
63
|
+
* the freshly-mounted sheet is visible on its first commit, with no
|
|
64
|
+
* present()-in-effect race that would otherwise leave it blank. Omit for the
|
|
65
|
+
* purely imperative `present()`/`dismiss()` API.
|
|
66
|
+
*/
|
|
67
|
+
open?: boolean;
|
|
58
68
|
onDismiss?: () => void;
|
|
59
69
|
enablePanDownToClose?: boolean;
|
|
60
70
|
backgroundComponent?: (props: { style?: StyleProp<ViewStyle> }) => React.ReactElement | null;
|
|
@@ -128,6 +138,19 @@ export interface BottomSheetProps {
|
|
|
128
138
|
* `showHandle` is `true`.
|
|
129
139
|
*/
|
|
130
140
|
handleComponent?: () => React.ReactNode;
|
|
141
|
+
/**
|
|
142
|
+
* External shared value the internal scroll handler mirrors the content
|
|
143
|
+
* offset into (in addition to its own gesture-gating offset). Lets a host —
|
|
144
|
+
* e.g. a Dialog nav-header — drive a collapse from the sheet's OWN scroller
|
|
145
|
+
* without owning the ScrollView. Optional; the standalone sheet ignores it.
|
|
146
|
+
*/
|
|
147
|
+
scrollY?: SharedValue<number>;
|
|
148
|
+
/**
|
|
149
|
+
* Overlay slot rendered absolutely at the TOP of the sheet card, painted
|
|
150
|
+
* above the scrolling body (clipped to the sheet's rounded top). Used by the
|
|
151
|
+
* Dialog to float its sticky nav header over the sheet's scroll content.
|
|
152
|
+
*/
|
|
153
|
+
headerOverlay?: React.ReactNode;
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
/**
|
|
@@ -169,6 +192,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
169
192
|
const {
|
|
170
193
|
Shell,
|
|
171
194
|
children,
|
|
195
|
+
open,
|
|
172
196
|
onDismiss,
|
|
173
197
|
enablePanDownToClose = true,
|
|
174
198
|
backgroundComponent,
|
|
@@ -183,14 +207,19 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
183
207
|
manualActivation = false,
|
|
184
208
|
dynamicBackdrop = false,
|
|
185
209
|
handleComponent,
|
|
210
|
+
scrollY: externalScrollY,
|
|
211
|
+
headerOverlay,
|
|
186
212
|
} = props;
|
|
187
213
|
|
|
188
214
|
const insets = useSafeAreaInsets();
|
|
189
215
|
const theme = useTheme();
|
|
190
216
|
const { colors } = theme;
|
|
191
217
|
const { height: screenHeight } = useScreenDimensions();
|
|
192
|
-
|
|
193
|
-
|
|
218
|
+
// Seed from the controlled `open` prop so a sheet that should be open is
|
|
219
|
+
// visible on its FIRST commit (no present()-in-effect race) — this is what
|
|
220
|
+
// lets a responsive Dialog survive a resize placement swap without going blank.
|
|
221
|
+
const [visible, setVisible] = useState(() => open ?? false);
|
|
222
|
+
const [rendered, setRendered] = useState(() => open ?? false); // keep mounted for exit animation
|
|
194
223
|
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
195
224
|
const hasClosedRef = useRef(false);
|
|
196
225
|
const scrollViewRef = useRef<Animated.ScrollView>(null);
|
|
@@ -266,6 +295,12 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
266
295
|
useEffect(() => {
|
|
267
296
|
renderedRef.current = rendered;
|
|
268
297
|
}, [rendered]);
|
|
298
|
+
// Mirror `visible` so the unmount cleanup can tell "closing" (visible=false,
|
|
299
|
+
// animating out) from "fully open" (visible=true) at teardown time.
|
|
300
|
+
const visibleRef = useRef(visible);
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
visibleRef.current = visible;
|
|
303
|
+
}, [visible]);
|
|
269
304
|
|
|
270
305
|
/**
|
|
271
306
|
* Commit a close. Two guards prevent stale callbacks from firing:
|
|
@@ -325,18 +360,24 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
325
360
|
}, [visible, rendered, finishClose, screenHeight, closeGeneration, opacity, translateY]);
|
|
326
361
|
|
|
327
362
|
// On unmount: ensure pending close callbacks (e.g. consumer's `onDismiss`)
|
|
328
|
-
// still fire if the BS is yanked mid-animation by a parent re-render
|
|
329
|
-
// Without this, `Dialog`'s `handleDismiss` never runs
|
|
330
|
-
// callbacks (post-close handlers) are silently lost.
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
363
|
+
// still fire if the BS is yanked mid-animation by a parent re-render while a
|
|
364
|
+
// close is IN PROGRESS. Without this, `Dialog`'s `handleDismiss` never runs
|
|
365
|
+
// and queued callbacks (post-close handlers) are silently lost.
|
|
366
|
+
//
|
|
367
|
+
// Guarded on `!visibleRef.current`: only flush when a close was already
|
|
368
|
+
// underway (visible=false → closing/animating out). A sheet yanked while
|
|
369
|
+
// still FULLY OPEN (visible=true) is not closing — it is being swapped out by
|
|
370
|
+
// a parent, e.g. a responsive `Dialog` crossing its breakpoint between the
|
|
371
|
+
// bottom-sheet and centered-card branches. Firing onDismiss there would
|
|
372
|
+
// wrongly dismiss the surface mid-swap (there are no pending close callbacks
|
|
373
|
+
// to flush anyway), so the new branch can take over still-open. Refs are read
|
|
374
|
+
// inside the cleanup, so the latest values are captured.
|
|
334
375
|
useEffect(() => () => {
|
|
335
376
|
if (closeTimeoutRef.current) {
|
|
336
377
|
clearTimeout(closeTimeoutRef.current);
|
|
337
378
|
closeTimeoutRef.current = null;
|
|
338
379
|
}
|
|
339
|
-
if (renderedRef.current && !hasClosedRef.current) {
|
|
380
|
+
if (renderedRef.current && !hasClosedRef.current && !visibleRef.current) {
|
|
340
381
|
hasClosedRef.current = true;
|
|
341
382
|
safeCloseRef.current();
|
|
342
383
|
}
|
|
@@ -639,8 +680,12 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
639
680
|
onScroll: (event) => {
|
|
640
681
|
scrollOffsetY.value = event.contentOffset.y;
|
|
641
682
|
isScrollAtTop.value = event.contentOffset.y <= 0;
|
|
683
|
+
// Mirror into the host's collapse offset (a Dialog nav header) when
|
|
684
|
+
// provided. Listed in deps so the RN-Web (no worklets plugin) handler
|
|
685
|
+
// re-binds if the shared value identity changes.
|
|
686
|
+
if (externalScrollY) externalScrollY.value = event.contentOffset.y;
|
|
642
687
|
},
|
|
643
|
-
}, []);
|
|
688
|
+
}, [externalScrollY]);
|
|
644
689
|
|
|
645
690
|
const dynamicStyles = useMemo(() => {
|
|
646
691
|
return StyleSheet.create({
|
|
@@ -741,6 +786,10 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
741
786
|
{handleSlot}
|
|
742
787
|
|
|
743
788
|
{bodyContent}
|
|
789
|
+
|
|
790
|
+
{/* Sticky nav-header overlay (Dialog nav-header mode): floats
|
|
791
|
+
above the scroll body, clipped to the sheet's rounded top. */}
|
|
792
|
+
{headerOverlay}
|
|
744
793
|
</Animated.View>
|
|
745
794
|
</GestureDetector>
|
|
746
795
|
</View>
|
|
@@ -12,7 +12,7 @@ import { useTheme } from '../theme/use-theme';
|
|
|
12
12
|
import { useControllableState } from '../hooks/useControllableState';
|
|
13
13
|
import { Text } from '../typography';
|
|
14
14
|
import { Item } from '../item';
|
|
15
|
-
import {
|
|
15
|
+
import { Search } from '../search';
|
|
16
16
|
import { borderRadius, fontSize, space } from '../styles/tokens';
|
|
17
17
|
import {
|
|
18
18
|
ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon,
|
|
@@ -55,7 +55,7 @@ function defaultFilter<T>(option: ComboboxOption<T>, query: string): boolean {
|
|
|
55
55
|
* `.web` resolution. The combobox logic is single-source.
|
|
56
56
|
*
|
|
57
57
|
* `Combobox` is a filterable autocomplete select that delegates its overlay to
|
|
58
|
-
* Bloom's `Popover` and composes `
|
|
58
|
+
* Bloom's `Popover` and composes `Search` + `Item`. The trigger + panel
|
|
59
59
|
* read the overlay control from `Popover` context (via an inner component) so
|
|
60
60
|
* selecting an option reliably closes the overlay on every platform.
|
|
61
61
|
*/
|
|
@@ -153,7 +153,7 @@ export function createCombobox(PopoverImpl: PopoverModule) {
|
|
|
153
153
|
|
|
154
154
|
<PopoverImpl.PopoverContent label={accessibleLabel} placement="bottom-start" minWidth={240}>
|
|
155
155
|
<View style={styles.searchWrap}>
|
|
156
|
-
<
|
|
156
|
+
<Search
|
|
157
157
|
ref={searchRef}
|
|
158
158
|
label={label ?? 'Search'}
|
|
159
159
|
value={query}
|
package/src/command/Command.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import { useControllableState } from '../hooks/useControllableState';
|
|
|
12
12
|
import { Text } from '../typography';
|
|
13
13
|
import { Item } from '../item';
|
|
14
14
|
import { Kbd } from '../kbd';
|
|
15
|
-
import {
|
|
15
|
+
import { Search } from '../search';
|
|
16
16
|
import { fontSize, space } from '../styles/tokens';
|
|
17
17
|
import type { DialogProps } from '../dialog';
|
|
18
18
|
import { useDialogControl } from '../dialog/context';
|
|
@@ -46,7 +46,7 @@ interface FlatEntry {
|
|
|
46
46
|
* relies on implicit `.web` resolution. The body is single-source.
|
|
47
47
|
*
|
|
48
48
|
* `Command` is a ⌘K command palette built on `<Dialog placement="center">`,
|
|
49
|
-
* `
|
|
49
|
+
* `Search`, an `Item` results list and `Kbd` shortcut hints. Items group
|
|
50
50
|
* by their `group` field (ungrouped first). On web the list is
|
|
51
51
|
* keyboard-navigable (Up/Down/Enter); on native, tap to select.
|
|
52
52
|
*
|
|
@@ -231,7 +231,7 @@ export function createCommand(Dialog: DialogComponent) {
|
|
|
231
231
|
testID={testID}>
|
|
232
232
|
<View {...webKeyHandler}>
|
|
233
233
|
<View style={[styles.searchWrap, { borderBottomColor: theme.colors.borderLight }]}>
|
|
234
|
-
<
|
|
234
|
+
<Search
|
|
235
235
|
ref={searchRef}
|
|
236
236
|
label={placeholder}
|
|
237
237
|
placeholder={placeholder}
|