@oxyhq/bloom 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/dialog/Dialog.js +45 -157
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +64 -198
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +216 -0
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -0
- package/lib/commonjs/dialog/DialogContent.js +155 -0
- package/lib/commonjs/dialog/DialogContent.js.map +1 -0
- package/lib/commonjs/dialog/placement.js +12 -1
- package/lib/commonjs/dialog/placement.js.map +1 -1
- package/lib/module/dialog/Dialog.js +48 -160
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +66 -200
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +211 -0
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -0
- package/lib/module/dialog/DialogContent.js +148 -0
- package/lib/module/dialog/DialogContent.js.map +1 -0
- package/lib/module/dialog/placement.js +11 -0
- package/lib/module/dialog/placement.js.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +11 -8
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +29 -0
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/DialogContent.d.ts +32 -0
- package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/placement.d.ts +10 -0
- package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +12 -0
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts +11 -8
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +29 -0
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -0
- package/lib/typescript/module/dialog/DialogContent.d.ts +32 -0
- package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -0
- package/lib/typescript/module/dialog/placement.d.ts +10 -0
- package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +12 -0
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/BottomSheetGesture.test.tsx +135 -0
- package/src/__tests__/DialogBottomSheet.test.tsx +259 -0
- package/src/dialog/Dialog.tsx +41 -171
- package/src/dialog/Dialog.web.tsx +56 -211
- package/src/dialog/DialogBottomSheet.tsx +250 -0
- package/src/dialog/DialogContent.tsx +157 -0
- package/src/dialog/placement.ts +11 -0
- package/src/dialog/types.ts +12 -0
|
@@ -4,9 +4,10 @@ import type { DialogAction, DialogProps } from './types';
|
|
|
4
4
|
* Web variant of `<Dialog>`.
|
|
5
5
|
*
|
|
6
6
|
* A centered modal card (default) rendered into the bloom Portal at the end of
|
|
7
|
-
* `document.body`,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* `document.body`, an anchored side-sheet for `left`/`right`, or — for
|
|
8
|
+
* `bottom` — bloom's cross-platform `BottomSheet` (the SAME component native
|
|
9
|
+
* uses), so the bottom placement drags-to-dismiss on web too. Same prop API as
|
|
10
|
+
* native, so call sites are platform agnostic.
|
|
10
11
|
*
|
|
11
12
|
* Open/close is driven by EITHER the imperative `control` (legacy) OR the
|
|
12
13
|
* controlled `open` prop. When `open` is provided it wins.
|
|
@@ -18,10 +19,11 @@ import type { DialogAction, DialogProps } from './types';
|
|
|
18
19
|
*
|
|
19
20
|
* Web motion never uses reanimated `exiting` (which throws `removeChild` on
|
|
20
21
|
* concurrent React unmounts): the centered card uses CSS keyframes and the
|
|
21
|
-
* side
|
|
22
|
-
*
|
|
22
|
+
* side modes use self-contained CSS transitions on a mounted-through-exit
|
|
23
|
+
* node. The bottom placement animates via `BottomSheet`'s shared-value/gesture
|
|
24
|
+
* animation (no `exiting` layout animation).
|
|
23
25
|
*/
|
|
24
|
-
export declare function Dialog({
|
|
26
|
+
export declare function Dialog({ placement, ...rest }: DialogProps): import("react/jsx-runtime").JSX.Element;
|
|
25
27
|
export { DIALOG_SHEET_BACKDROP_TESTID };
|
|
26
28
|
/**
|
|
27
29
|
* Inline imperative dialog used by `alert()`. Mounts and immediately
|
|
@@ -45,8 +47,9 @@ export declare function AutoMountedDialog({ title, description, actions, onResol
|
|
|
45
47
|
* @keyframes bloomDialogZoomFadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } }
|
|
46
48
|
* ```
|
|
47
49
|
*
|
|
48
|
-
* The `left`/`right
|
|
49
|
-
*
|
|
50
|
+
* The `left`/`right` placements do NOT depend on this — they animate via
|
|
51
|
+
* self-contained inline CSS transitions and need no keyframe injection. The
|
|
52
|
+
* `bottom` placement uses bloom's `BottomSheet` (reanimated) and needs none.
|
|
50
53
|
*/
|
|
51
54
|
export declare const BLOOM_DIALOG_CSS = "\n@keyframes bloomDialogFadeIn { from { opacity: 0; } to { opacity: 1; } }\n@keyframes bloomDialogFadeOut { from { opacity: 1; } to { opacity: 0; } }\n@keyframes bloomDialogZoomFadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }\n@keyframes bloomDialogZoomFadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } }\n";
|
|
52
55
|
//# sourceMappingURL=Dialog.web.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AAyBA,OAAO,EAML,4BAA4B,EAM7B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,YAAY,EAGZ,WAAW,EACZ,MAAM,SAAS,CAAC;AASjB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,2CAazD;AAoSD,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAqNxC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,GACV,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,2CAgBA;AA6BD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,mZAK5B,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DialogProps } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Shared `BottomSheet`-backed surface for the `bottom` placement, used by BOTH
|
|
4
|
+
* `Dialog.tsx` (native) and `Dialog.web.tsx` (web). `BottomSheet` is itself a
|
|
5
|
+
* single cross-platform implementation (react-native-gesture-handler +
|
|
6
|
+
* reanimated, drag-to-dismiss on web and native alike), so routing the bottom
|
|
7
|
+
* placement through here gives ONE code path with drag working everywhere — no
|
|
8
|
+
* duplicated CSS slide-up sheet.
|
|
9
|
+
*
|
|
10
|
+
* This component owns the entire open↔present/dismiss bridge and the Dialog →
|
|
11
|
+
* BottomSheet prop mapping in one place:
|
|
12
|
+
*
|
|
13
|
+
* - `open` (controlled) / `control` (imperative) → `present()` / `dismiss()`
|
|
14
|
+
* - `onClose` → fired once the sheet settles
|
|
15
|
+
* - `maxHeightRatio` → sheet `maxHeight`
|
|
16
|
+
* - `showHandle` / `dismissOnBackdrop` / `label` → forwarded to the surface
|
|
17
|
+
* - `panelStyle` → paints the sheet surface
|
|
18
|
+
* - `containerStyle` → wraps the content subtree
|
|
19
|
+
* so a host's CSS-var theme scope (`vars()`) still applies to descendants
|
|
20
|
+
* - `title` / `description` / `actions` / `children` → declarative body
|
|
21
|
+
*/
|
|
22
|
+
export declare function DialogBottomSheet({ control, open: controlledOpen, onClose, testID, title, description, actions, maxHeightRatio, showHandle, dismissOnBackdrop, contentPadding, style, panelStyle, panelClassName, containerStyle, containerClassName, label, children, }: DialogBottomSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
/**
|
|
24
|
+
* Props the shared bottom-sheet surface consumes — the `bottom`-relevant subset
|
|
25
|
+
* of `DialogProps`. `width`, `maxWidth`, and `inset` are placement-specific to
|
|
26
|
+
* the side/centered surfaces and intentionally excluded.
|
|
27
|
+
*/
|
|
28
|
+
export type DialogBottomSheetProps = Pick<DialogProps, 'control' | 'open' | 'onClose' | 'testID' | 'title' | 'description' | 'actions' | 'maxHeightRatio' | 'showHandle' | 'dismissOnBackdrop' | 'contentPadding' | 'style' | 'panelStyle' | 'panelClassName' | 'containerStyle' | 'containerClassName' | 'label' | 'children'>;
|
|
29
|
+
//# sourceMappingURL=DialogBottomSheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DialogBottomSheet.d.ts","sourceRoot":"","sources":["../../../../src/dialog/DialogBottomSheet.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,IAAI,EAAE,cAAc,EACpB,OAAO,EACP,MAAM,EACN,KAAK,EACL,WAAW,EACX,OAAO,EACP,cAAyC,EACzC,UAAiB,EACjB,iBAAwB,EACxB,cAA+C,EAC/C,KAAK,EACL,UAAU,EACV,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,KAAK,EACL,QAAQ,GACT,EAAE,sBAAsB,2CAiKxB;AAED;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,WAAW,EACT,SAAS,GACT,MAAM,GACN,SAAS,GACT,QAAQ,GACR,OAAO,GACP,aAAa,GACb,SAAS,GACT,gBAAgB,GAChB,YAAY,GACZ,mBAAmB,GACnB,gBAAgB,GAChB,OAAO,GACP,YAAY,GACZ,gBAAgB,GAChB,gBAAgB,GAChB,oBAAoB,GACpB,OAAO,GACP,UAAU,CACb,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ThemeColors } from '../theme/types';
|
|
3
|
+
import type { DialogAction, DialogActionColor } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Shared dialog content primitives used by every `<Dialog>` placement on both
|
|
6
|
+
* platforms — the centered panel, the side-sheets, and the `BottomSheet`-backed
|
|
7
|
+
* bottom placement. Keeping them here is the single source of truth for the
|
|
8
|
+
* declarative `title` / `description` / `actions` chrome, so the surfaces never
|
|
9
|
+
* drift in spacing, typography, or action-button behaviour.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Renders the dialog's body: optional declarative title + description, any
|
|
13
|
+
* `children`, then the action row. The `titleId`/`descriptionId` are wired by
|
|
14
|
+
* the caller onto the dialog role element for `aria-labelledby` /
|
|
15
|
+
* `aria-describedby`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function DialogBody({ titleId, descriptionId, title, description, actions, children, }: {
|
|
18
|
+
titleId: string;
|
|
19
|
+
descriptionId: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
actions?: DialogAction[];
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare function ActionRow({ actions }: {
|
|
26
|
+
actions: DialogAction[];
|
|
27
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export declare function getActionPalette(color: DialogActionColor, colors: ThemeColors): {
|
|
29
|
+
background: string;
|
|
30
|
+
foreground: string;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=DialogContent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DialogContent.d.ts","sourceRoot":"","sources":["../../../../src/dialog/DialogContent.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsB,MAAM,OAAO,CAAC;AAQ3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;GAMG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,aAAa,EACb,KAAK,EACL,WAAW,EACX,OAAO,EACP,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,2CAmCA;AAED,wBAAgB,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,YAAY,EAAE,CAAA;CAAE,2CAQjE;AAkDD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAClB;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAiB5C"}
|
|
@@ -40,6 +40,16 @@ export declare const DEFAULT_CENTER_MAX_WIDTH = 480;
|
|
|
40
40
|
export declare const DEFAULT_MAX_HEIGHT_RATIO = 0.9;
|
|
41
41
|
/** Corner radius shared by the side-sheet panel and the bottom-sheet top. */
|
|
42
42
|
export declare const PANEL_RADIUS = 20;
|
|
43
|
+
/**
|
|
44
|
+
* Default inner padding (px) of the dialog content container, applied uniformly
|
|
45
|
+
* across every placement (centered panel, side-sheet body, bottom-sheet body)
|
|
46
|
+
* so the chrome reads identically regardless of which surface resolves. This is
|
|
47
|
+
* the legacy value — omitting the `contentPadding` prop keeps 20px everywhere,
|
|
48
|
+
* byte-for-byte unchanged. A host that owns its own insets (e.g. custom
|
|
49
|
+
* `children` that already manage padding) can override it via
|
|
50
|
+
* `Dialog`'s `contentPadding` prop (e.g. `contentPadding={0}`).
|
|
51
|
+
*/
|
|
52
|
+
export declare const DEFAULT_DIALOG_CONTENT_PADDING = 20;
|
|
43
53
|
/** Open/close transition duration (ms). ~280ms ease-out reads smooth. */
|
|
44
54
|
export declare const ANIMATION_DURATION = 280;
|
|
45
55
|
/** Centered-card fade-out duration (ms) — preserves the legacy timing. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../../../src/dialog/placement.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE;;;;;;;;;GASG;AACH,MAAM,MAAM,yBAAyB,GACjC,eAAe,GACf;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;CACtB,CAAC;AAEN;;;;GAIG;AACH,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,aAAa,OAAO,CAAC;AAClC,eAAO,MAAM,aAAa,OAAO,CAAC;AAUlC,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,4EAA4E;AAC5E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,6EAA6E;AAC7E,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0EAA0E;AAC1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,oEAAoE;AACpE,eAAO,MAAM,QAAQ,mCAAmC,CAAC;AAEzD,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,gCAAgC,CAAC;AAE1E,sDAAsD;AACtD,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B;;;GAGG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,yBAAyB,GAAG,SAAS,GAC/C,eAAe,CAcjB"}
|
|
1
|
+
{"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../../../src/dialog/placement.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE;;;;;;;;;GASG;AACH,MAAM,MAAM,yBAAyB,GACjC,eAAe,GACf;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;CACtB,CAAC;AAEN;;;;GAIG;AACH,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,aAAa,OAAO,CAAC;AAClC,eAAO,MAAM,aAAa,OAAO,CAAC;AAUlC,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,4EAA4E;AAC5E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,6EAA6E;AAC7E,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAEjD,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0EAA0E;AAC1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,oEAAoE;AACpE,eAAO,MAAM,QAAQ,mCAAmC,CAAC;AAEzD,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,gCAAgC,CAAC;AAE1E,sDAAsD;AACtD,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B;;;GAGG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,yBAAyB,GAAG,SAAS,GAC/C,eAAe,CAcjB"}
|
|
@@ -125,6 +125,18 @@ export type DialogProps = React.PropsWithChildren<{
|
|
|
125
125
|
inset?: DialogInset;
|
|
126
126
|
/** Whether to render the drag handle in bottom-sheet mode. Defaults to `true`. */
|
|
127
127
|
showHandle?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Inner padding (px) of the dialog content container, applied uniformly across
|
|
130
|
+
* every placement (centered panel, side-sheet body, bottom-sheet body).
|
|
131
|
+
* Defaults to `20`.
|
|
132
|
+
*
|
|
133
|
+
* The default chrome padding is correct for the declarative
|
|
134
|
+
* `title`/`description`/`actions` layout, but a host passing pure custom
|
|
135
|
+
* `children` that already own their insets can set `contentPadding={0}` to
|
|
136
|
+
* render flush content and manage padding itself. Omitting the prop keeps the
|
|
137
|
+
* 20px default on every surface.
|
|
138
|
+
*/
|
|
139
|
+
contentPadding?: number;
|
|
128
140
|
/** Whether tapping the backdrop dismisses the dialog. Defaults to `true`. */
|
|
129
141
|
dismissOnBackdrop?: boolean;
|
|
130
142
|
/**
|
|
@@ -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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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, { createRef } from 'react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
import { act, render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
6
|
+
import BottomSheet, { type BottomSheetRef } from '../bottom-sheet';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pan-to-dismiss is the whole point of routing the Dialog `bottom` placement
|
|
10
|
+
* through `BottomSheet` on BOTH platforms. These tests assert the pan gesture
|
|
11
|
+
* is actually wired (an `onEnd` worklet is registered) and that the
|
|
12
|
+
* dismiss-threshold logic inside it runs end-to-end — i.e. a fast/far downward
|
|
13
|
+
* swipe commits the close and fires `onDismiss`, while a small drag springs the
|
|
14
|
+
* sheet back without dismissing.
|
|
15
|
+
*
|
|
16
|
+
* This file overrides the default gesture-handler mock (which discards the
|
|
17
|
+
* registered callbacks) with a RECORDING mock that captures the pan gesture's
|
|
18
|
+
* handlers. Invoking the captured `onEnd` exercises the real threshold math
|
|
19
|
+
* from `bottom-sheet/index.tsx` — there is no `Platform.OS` gating around the
|
|
20
|
+
* `<GestureDetector>` or the pan builder, so this is the SAME code path web
|
|
21
|
+
* runs (drag-to-dismiss is active on web).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface PanEndEvent {
|
|
25
|
+
velocityY: number;
|
|
26
|
+
translationY: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RecordedGesture {
|
|
30
|
+
onEnd?: (event: PanEndEvent) => void;
|
|
31
|
+
onUpdate?: (event: { translationY: number }) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Recorded `Gesture.Pan()` instances from the most recent render. Must be
|
|
35
|
+
// `mock`-prefixed so the hoisted `jest.mock` factory may reference it.
|
|
36
|
+
const mockPanGestures: RecordedGesture[] = [];
|
|
37
|
+
|
|
38
|
+
jest.mock('react-native-gesture-handler', () => {
|
|
39
|
+
const builder = (record?: RecordedGesture) => {
|
|
40
|
+
const self = {
|
|
41
|
+
enabled: () => self,
|
|
42
|
+
simultaneousWithExternalGesture: () => self,
|
|
43
|
+
withRef: () => self,
|
|
44
|
+
manualActivation: () => self,
|
|
45
|
+
activeOffsetY: () => self,
|
|
46
|
+
activeOffsetX: () => self,
|
|
47
|
+
onStart: () => self,
|
|
48
|
+
onUpdate: (fn: RecordedGesture['onUpdate']) => {
|
|
49
|
+
if (record) record.onUpdate = fn;
|
|
50
|
+
return self;
|
|
51
|
+
},
|
|
52
|
+
onEnd: (fn: RecordedGesture['onEnd']) => {
|
|
53
|
+
if (record) record.onEnd = fn;
|
|
54
|
+
return self;
|
|
55
|
+
},
|
|
56
|
+
onTouchesDown: () => self,
|
|
57
|
+
onTouchesMove: () => self,
|
|
58
|
+
onTouchesUp: () => self,
|
|
59
|
+
onTouchesCancelled: () => self,
|
|
60
|
+
};
|
|
61
|
+
return self;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
Gesture: {
|
|
66
|
+
Pan: () => {
|
|
67
|
+
const record: RecordedGesture = {};
|
|
68
|
+
mockPanGestures.push(record);
|
|
69
|
+
return builder(record);
|
|
70
|
+
},
|
|
71
|
+
Native: () => builder(),
|
|
72
|
+
},
|
|
73
|
+
GestureDetector: ({ children }: { children: React.ReactNode }) => children,
|
|
74
|
+
GestureHandlerRootView: ({ children }: { children: React.ReactNode }) => children,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function renderSheet(onDismiss: () => void) {
|
|
79
|
+
const ref = createRef<BottomSheetRef>();
|
|
80
|
+
const utils = render(
|
|
81
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
82
|
+
<BottomSheet ref={ref} onDismiss={onDismiss}>
|
|
83
|
+
<Text>Body</Text>
|
|
84
|
+
</BottomSheet>
|
|
85
|
+
</BloomThemeProvider>,
|
|
86
|
+
);
|
|
87
|
+
// Present so the sheet mounts and the pan gesture is constructed.
|
|
88
|
+
act(() => {
|
|
89
|
+
ref.current?.present();
|
|
90
|
+
});
|
|
91
|
+
return utils;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('BottomSheet pan-to-dismiss gesture', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
mockPanGestures.length = 0;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('wires a pan gesture with an onEnd handler (drag is active, not web-gated)', () => {
|
|
100
|
+
renderSheet(() => {});
|
|
101
|
+
// A pan gesture must have been constructed with an onEnd worklet — proof the
|
|
102
|
+
// drag pipeline is built unconditionally (the source has no
|
|
103
|
+
// `Platform.OS !== 'web'` guard around the GestureDetector or pan builder).
|
|
104
|
+
expect(mockPanGestures.length).toBeGreaterThan(0);
|
|
105
|
+
const bodyPan = mockPanGestures.find((g) => typeof g.onEnd === 'function');
|
|
106
|
+
expect(bodyPan).toBeDefined();
|
|
107
|
+
expect(typeof bodyPan?.onEnd).toBe('function');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('dismisses when a fast downward swipe crosses the close threshold', () => {
|
|
111
|
+
const onDismiss = jest.fn();
|
|
112
|
+
renderSheet(onDismiss);
|
|
113
|
+
const bodyPan = mockPanGestures.find((g) => typeof g.onEnd === 'function');
|
|
114
|
+
|
|
115
|
+
// A high downward velocity is past `fastSwipeThreshold` (900) → the onEnd
|
|
116
|
+
// worklet commits the close, which (under the reanimated mock that runs
|
|
117
|
+
// withTiming callbacks synchronously) drains through to `onDismiss`.
|
|
118
|
+
act(() => {
|
|
119
|
+
bodyPan?.onEnd?.({ velocityY: 1500, translationY: 50 });
|
|
120
|
+
});
|
|
121
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not dismiss on a small, slow drag below the threshold', () => {
|
|
125
|
+
const onDismiss = jest.fn();
|
|
126
|
+
renderSheet(onDismiss);
|
|
127
|
+
const bodyPan = mockPanGestures.find((g) => typeof g.onEnd === 'function');
|
|
128
|
+
|
|
129
|
+
// Tiny translation + near-zero velocity → springs back, no close.
|
|
130
|
+
act(() => {
|
|
131
|
+
bodyPan?.onEnd?.({ velocityY: 10, translationY: 12 });
|
|
132
|
+
});
|
|
133
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { createRef } from 'react';
|
|
2
|
+
import { Text, type View } from 'react-native';
|
|
3
|
+
import { act, fireEvent, render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { Dialog, useDialogControl } from '../dialog';
|
|
6
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Minimal shape of a `react-test-renderer` JSON node — enough to walk the tree
|
|
10
|
+
* by host `type` string without depending on `@types/react-test-renderer`.
|
|
11
|
+
*/
|
|
12
|
+
type RenderedNode = {
|
|
13
|
+
type: string;
|
|
14
|
+
children: Array<RenderedNode | string> | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Count host nodes of a given `type` in a rendered `toJSON()` tree. */
|
|
18
|
+
function countNodesByType(
|
|
19
|
+
tree: RenderedNode | RenderedNode[] | null,
|
|
20
|
+
type: string,
|
|
21
|
+
): number {
|
|
22
|
+
if (tree === null) return 0;
|
|
23
|
+
const roots = Array.isArray(tree) ? tree : [tree];
|
|
24
|
+
let count = 0;
|
|
25
|
+
for (const node of roots) {
|
|
26
|
+
if (node.type === type) count += 1;
|
|
27
|
+
if (node.children) {
|
|
28
|
+
for (const child of node.children) {
|
|
29
|
+
if (typeof child !== 'string') count += countNodesByType(child, type);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return count;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The Dialog `bottom` placement must delegate to the shared cross-platform
|
|
38
|
+
* `BottomSheet` (via `DialogBottomSheet`) — the SAME surface native and web use
|
|
39
|
+
* — instead of a bespoke CSS slide-up sheet. These tests pin that delegation:
|
|
40
|
+
* the declarative content renders through the sheet, the close lifecycle bridges
|
|
41
|
+
* `open`/`onClose` correctly, and brand theming (`panelStyle` surface paint +
|
|
42
|
+
* `containerStyle` theme scope) threads through to the sheet surface.
|
|
43
|
+
*
|
|
44
|
+
* The jest environment resolves `../dialog` to the native barrel (`Dialog.tsx`),
|
|
45
|
+
* which routes `bottom` through `DialogBottomSheet` → `BottomSheet`. The web
|
|
46
|
+
* fork (`Dialog.web.tsx`) routes through the EXACT same `DialogBottomSheet`, so
|
|
47
|
+
* this delegation is one shared code path across platforms.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
51
|
+
return render(
|
|
52
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
53
|
+
{ui}
|
|
54
|
+
</BloomThemeProvider>,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function Harness({
|
|
59
|
+
children,
|
|
60
|
+
}: {
|
|
61
|
+
children: (control: ReturnType<typeof useDialogControl>) => React.ReactNode;
|
|
62
|
+
}) {
|
|
63
|
+
const control = useDialogControl();
|
|
64
|
+
return <>{children(control)}</>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('Dialog bottom placement delegates to BottomSheet', () => {
|
|
68
|
+
it('does not render bottom-sheet content until opened', () => {
|
|
69
|
+
const { queryByText } = renderWithTheme(
|
|
70
|
+
<Harness>
|
|
71
|
+
{(control) => (
|
|
72
|
+
<Dialog control={control} placement="bottom" title="Sheet title">
|
|
73
|
+
<Text>Sheet body</Text>
|
|
74
|
+
</Dialog>
|
|
75
|
+
)}
|
|
76
|
+
</Harness>,
|
|
77
|
+
);
|
|
78
|
+
// BottomSheet keeps its subtree unmounted until present() — proof the bottom
|
|
79
|
+
// placement is the sheet, not an always-mounted CSS panel.
|
|
80
|
+
expect(queryByText('Sheet title')).toBeNull();
|
|
81
|
+
expect(queryByText('Sheet body')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders declarative title + description + actions through the sheet when opened', () => {
|
|
85
|
+
let control: ReturnType<typeof useDialogControl> | undefined;
|
|
86
|
+
const { getByText } = renderWithTheme(
|
|
87
|
+
<Harness>
|
|
88
|
+
{(c) => {
|
|
89
|
+
control = c;
|
|
90
|
+
return (
|
|
91
|
+
<Dialog
|
|
92
|
+
control={c}
|
|
93
|
+
placement="bottom"
|
|
94
|
+
title="Delete item?"
|
|
95
|
+
description="This cannot be undone."
|
|
96
|
+
actions={[{ label: 'Delete', color: 'destructive' }, { label: 'Cancel', color: 'cancel' }]}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}}
|
|
100
|
+
</Harness>,
|
|
101
|
+
);
|
|
102
|
+
act(() => {
|
|
103
|
+
control?.open();
|
|
104
|
+
});
|
|
105
|
+
expect(getByText('Delete item?')).toBeTruthy();
|
|
106
|
+
expect(getByText('This cannot be undone.')).toBeTruthy();
|
|
107
|
+
expect(getByText('Delete')).toBeTruthy();
|
|
108
|
+
expect(getByText('Cancel')).toBeTruthy();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('requests close via onClose exactly once on action press in controlled mode (no double-fire)', () => {
|
|
112
|
+
const onClose = jest.fn();
|
|
113
|
+
const { getByText } = renderWithTheme(
|
|
114
|
+
<Dialog open onClose={onClose} placement="bottom" title="Confirm" actions={[{ label: 'Done' }]} />,
|
|
115
|
+
);
|
|
116
|
+
act(() => {
|
|
117
|
+
fireEvent.press(getByText('Done'));
|
|
118
|
+
});
|
|
119
|
+
// Controlled mode: the action's close() requests dismissal via onClose once;
|
|
120
|
+
// the host (not the dialog) then flips `open`. It must not fire the
|
|
121
|
+
// close-completion onClose a second time.
|
|
122
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('threads panelStyle (brand surface) and containerStyle (theme scope) onto the sheet content', () => {
|
|
126
|
+
let control: ReturnType<typeof useDialogControl> | undefined;
|
|
127
|
+
const panelRef = createRef<View>();
|
|
128
|
+
const { getByTestId } = renderWithTheme(
|
|
129
|
+
<Harness>
|
|
130
|
+
{(c) => {
|
|
131
|
+
control = c;
|
|
132
|
+
return (
|
|
133
|
+
<Dialog
|
|
134
|
+
control={c}
|
|
135
|
+
placement="bottom"
|
|
136
|
+
testID="brand-sheet"
|
|
137
|
+
panelStyle={{ backgroundColor: 'rebeccapurple' }}
|
|
138
|
+
containerStyle={{ paddingTop: 7 }}
|
|
139
|
+
>
|
|
140
|
+
<Text ref={panelRef}>Branded</Text>
|
|
141
|
+
</Dialog>
|
|
142
|
+
);
|
|
143
|
+
}}
|
|
144
|
+
</Harness>,
|
|
145
|
+
);
|
|
146
|
+
act(() => {
|
|
147
|
+
control?.open();
|
|
148
|
+
});
|
|
149
|
+
const content = getByTestId('brand-sheet');
|
|
150
|
+
const flattened = Array.isArray(content.props.style)
|
|
151
|
+
? Object.assign({}, ...content.props.style.filter(Boolean))
|
|
152
|
+
: content.props.style;
|
|
153
|
+
// panelStyle paints the surface; containerStyle (theme-var scope) wraps the
|
|
154
|
+
// content subtree. Both land on the sheet content container.
|
|
155
|
+
expect(flattened.backgroundColor).toBe('rebeccapurple');
|
|
156
|
+
expect(flattened.paddingTop).toBe(7);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('applies the default 20px content padding when contentPadding is omitted', () => {
|
|
160
|
+
let control: ReturnType<typeof useDialogControl> | undefined;
|
|
161
|
+
const { getByTestId } = renderWithTheme(
|
|
162
|
+
<Harness>
|
|
163
|
+
{(c) => {
|
|
164
|
+
control = c;
|
|
165
|
+
return (
|
|
166
|
+
<Dialog control={c} placement="bottom" testID="default-pad">
|
|
167
|
+
<Text>Body</Text>
|
|
168
|
+
</Dialog>
|
|
169
|
+
);
|
|
170
|
+
}}
|
|
171
|
+
</Harness>,
|
|
172
|
+
);
|
|
173
|
+
act(() => {
|
|
174
|
+
control?.open();
|
|
175
|
+
});
|
|
176
|
+
const content = getByTestId('default-pad');
|
|
177
|
+
const flattened = Array.isArray(content.props.style)
|
|
178
|
+
? Object.assign({}, ...content.props.style.filter(Boolean))
|
|
179
|
+
: content.props.style;
|
|
180
|
+
// Omitting `contentPadding` keeps the legacy 20px chrome padding.
|
|
181
|
+
expect(flattened.padding).toBe(20);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('removes the body padding when contentPadding={0} (host owns its insets)', () => {
|
|
185
|
+
let control: ReturnType<typeof useDialogControl> | undefined;
|
|
186
|
+
const { getByTestId } = renderWithTheme(
|
|
187
|
+
<Harness>
|
|
188
|
+
{(c) => {
|
|
189
|
+
control = c;
|
|
190
|
+
return (
|
|
191
|
+
<Dialog control={c} placement="bottom" testID="flush-pad" contentPadding={0}>
|
|
192
|
+
<Text>Flush body</Text>
|
|
193
|
+
</Dialog>
|
|
194
|
+
);
|
|
195
|
+
}}
|
|
196
|
+
</Harness>,
|
|
197
|
+
);
|
|
198
|
+
act(() => {
|
|
199
|
+
control?.open();
|
|
200
|
+
});
|
|
201
|
+
const content = getByTestId('flush-pad');
|
|
202
|
+
const flattened = Array.isArray(content.props.style)
|
|
203
|
+
? Object.assign({}, ...content.props.style.filter(Boolean))
|
|
204
|
+
: content.props.style;
|
|
205
|
+
// `contentPadding={0}` yields flush content the host pads itself.
|
|
206
|
+
expect(flattened.padding).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('does NOT wrap pure custom children in a second scroll container (no scroll-in-scroll)', () => {
|
|
210
|
+
let control: ReturnType<typeof useDialogControl> | undefined;
|
|
211
|
+
const result = renderWithTheme(
|
|
212
|
+
<Harness>
|
|
213
|
+
{(c) => {
|
|
214
|
+
control = c;
|
|
215
|
+
return (
|
|
216
|
+
<Dialog control={c} placement="bottom">
|
|
217
|
+
<Text>Custom children own their own scrolling</Text>
|
|
218
|
+
</Dialog>
|
|
219
|
+
);
|
|
220
|
+
}}
|
|
221
|
+
</Harness>,
|
|
222
|
+
);
|
|
223
|
+
act(() => {
|
|
224
|
+
control?.open();
|
|
225
|
+
});
|
|
226
|
+
// Pure custom children → DialogBottomSheet passes `scrollable={false}` to
|
|
227
|
+
// BottomSheet, so the sheet does NOT add its internal ScrollView. A child
|
|
228
|
+
// that contains its own scroller is then the ONLY scroll container.
|
|
229
|
+
const tree: RenderedNode | RenderedNode[] | null = result.toJSON();
|
|
230
|
+
expect(countNodesByType(tree, 'Animated.ScrollView')).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('keeps the internal scrollable body for declarative dialogs (title/description/actions)', () => {
|
|
234
|
+
let control: ReturnType<typeof useDialogControl> | undefined;
|
|
235
|
+
const result = renderWithTheme(
|
|
236
|
+
<Harness>
|
|
237
|
+
{(c) => {
|
|
238
|
+
control = c;
|
|
239
|
+
return (
|
|
240
|
+
<Dialog
|
|
241
|
+
control={c}
|
|
242
|
+
placement="bottom"
|
|
243
|
+
title="Confirm"
|
|
244
|
+
description="Short declarative body."
|
|
245
|
+
actions={[{ label: 'OK' }]}
|
|
246
|
+
/>
|
|
247
|
+
);
|
|
248
|
+
}}
|
|
249
|
+
</Harness>,
|
|
250
|
+
);
|
|
251
|
+
act(() => {
|
|
252
|
+
control?.open();
|
|
253
|
+
});
|
|
254
|
+
// Declarative chrome → the default scrollable body is retained, so a long
|
|
255
|
+
// title/description list still scrolls gracefully on a small viewport.
|
|
256
|
+
const tree: RenderedNode | RenderedNode[] | null = result.toJSON();
|
|
257
|
+
expect(countNodesByType(tree, 'Animated.ScrollView')).toBe(1);
|
|
258
|
+
});
|
|
259
|
+
});
|