@slithy/modal-spring 0.1.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/README.md +102 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +600 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @slithy/modal-spring
|
|
2
|
+
|
|
3
|
+
Animated modal adapter for `@slithy/modal-kit`, built on react-spring and @use-gesture/react.
|
|
4
|
+
|
|
5
|
+
Provides animated enter/leave transitions, an animated backdrop, and drag-to-close gesture support. For full usage documentation see [`docs/modal-implementation-guide.md`](../../docs/modal-implementation-guide.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @slithy/modal-core @slithy/modal-kit @slithy/modal-spring
|
|
13
|
+
pnpm add @react-spring/web @use-gesture/react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@react-spring/web` and `@use-gesture/react` are peer dependencies.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { LayerProvider, LayerStackPriority } from '@slithy/layers'
|
|
24
|
+
import { Portal } from '@slithy/portal'
|
|
25
|
+
import { SpringModalRenderer } from '@slithy/modal-spring'
|
|
26
|
+
|
|
27
|
+
export function App() {
|
|
28
|
+
return (
|
|
29
|
+
<LayerProvider id="app" zIndex={LayerStackPriority.Base}>
|
|
30
|
+
<main>{/* your app */}</main>
|
|
31
|
+
<SpringModalRenderer
|
|
32
|
+
renderLayer={(children) => (
|
|
33
|
+
<LayerProvider id="modal" zIndex={LayerStackPriority.Modal}>
|
|
34
|
+
{children}
|
|
35
|
+
</LayerProvider>
|
|
36
|
+
)}
|
|
37
|
+
renderPortal={(children) => <Portal>{children}</Portal>}
|
|
38
|
+
/>
|
|
39
|
+
</LayerProvider>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { useModalStore } from '@slithy/modal-core'
|
|
50
|
+
import { SpringModal } from '@slithy/modal-spring'
|
|
51
|
+
|
|
52
|
+
function MyButton() {
|
|
53
|
+
const open = (event: React.MouseEvent) => {
|
|
54
|
+
useModalStore.getState().openModal(
|
|
55
|
+
<SpringModal aria-label="My Modal">
|
|
56
|
+
<p>Content</p>
|
|
57
|
+
</SpringModal>,
|
|
58
|
+
{ triggerEvent: event }
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
return <button onClick={open}>Open</button>
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## SpringModal Props
|
|
68
|
+
|
|
69
|
+
| Prop | Type | Default | Description |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| `aria-label` | `string` | — | Accessible name for the dialog |
|
|
72
|
+
| `alignX` | `'center' \| 'left' \| 'right'` | `'center'` | Horizontal position |
|
|
73
|
+
| `alignY` | `'middle' \| 'top' \| 'bottom'` | `'middle'` | Vertical position |
|
|
74
|
+
| `dismissible` | `boolean` | `true` | Allow Escape and backdrop-click to close |
|
|
75
|
+
| `contentClassName` | `string` | — | Class on the `<dialog>` element |
|
|
76
|
+
| `contentStyle` | `CSSProperties` | — | Static styles on the `<dialog>` element |
|
|
77
|
+
| `contentTransitions` | `{ from, enter, leave }` | — | Spring transition values |
|
|
78
|
+
| `disableOpacityTransition` | `boolean` | — | Skip the default opacity fade |
|
|
79
|
+
| `dragDirection` | `DragDirection` | — | Enable drag-to-close |
|
|
80
|
+
| `containerScrolling` | `boolean` | `true` | Allow container scroll |
|
|
81
|
+
| `layerIsActive` | `boolean` | `true` | Pass from `useLayerState` for layer coordination |
|
|
82
|
+
| `springConfig` | `SpringConfig` | — | Override the default spring config |
|
|
83
|
+
| `afterOpen` | `() => void` | — | Fires after enter animation completes |
|
|
84
|
+
| `afterClose` | `() => void` | — | Fires after modal is removed |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Exports
|
|
89
|
+
|
|
90
|
+
| Export | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `SpringModal` | Animated modal component |
|
|
93
|
+
| `SpringModalRenderer` | Renders all open modals with animated backdrop |
|
|
94
|
+
| `DragHandle` | Drag-to-close gesture wrapper |
|
|
95
|
+
| `useModalDrag` | Low-level drag hook |
|
|
96
|
+
| `Backdrop` | Animated backdrop (used by `SpringModalRenderer`) |
|
|
97
|
+
| `defaultSpring` | Default spring config |
|
|
98
|
+
| `iosSheetSpring` | iOS-feel spring config |
|
|
99
|
+
| `useModalDragging` | Internal drag spring hook |
|
|
100
|
+
| `DragDirection` | `'up' \| 'down' \| 'left' \| 'right'` |
|
|
101
|
+
| `DragStyles` | Spring transform values from drag |
|
|
102
|
+
| `SpringModalProps` | — |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { SpringValues, SpringConfig, SpringValue, UseTransitionProps } from '@react-spring/web';
|
|
3
|
+
import { CSSProperties, RefObject, DOMAttributes } from 'react';
|
|
4
|
+
import { ModalProps as ModalProps$1, ModalRendererProps as ModalRendererProps$1 } from '@slithy/modal-kit';
|
|
5
|
+
import { ModalElement } from '@slithy/modal-core';
|
|
6
|
+
|
|
7
|
+
type BackdropProps = {
|
|
8
|
+
className?: string;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
/** Static CSS overrides applied alongside the animated spring styles. */
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
/** Animated spring values (or plain CSSProperties for static use). */
|
|
13
|
+
styles?: SpringValues<CSSProperties> | CSSProperties;
|
|
14
|
+
};
|
|
15
|
+
declare const ModalBackdrop: ({ className, onClick, style, styles, }: BackdropProps) => react_jsx_runtime.JSX.Element;
|
|
16
|
+
|
|
17
|
+
/** Default spring for modal enter/leave animations. */
|
|
18
|
+
declare const defaultSpring: SpringConfig;
|
|
19
|
+
/**
|
|
20
|
+
* iOS Sheet / Ionic Framework easing curve.
|
|
21
|
+
* 500ms duration — well-suited for bottom-sheet style drawers.
|
|
22
|
+
* cubic-bezier(0.32, 0.72, 0, 1)
|
|
23
|
+
*/
|
|
24
|
+
declare const iosSheetSpring: SpringConfig;
|
|
25
|
+
|
|
26
|
+
type DragDirection = 'down' | 'up' | 'left' | 'right';
|
|
27
|
+
type DragStyles = {
|
|
28
|
+
x?: SpringValue<number>;
|
|
29
|
+
y?: SpringValue<number>;
|
|
30
|
+
};
|
|
31
|
+
type UseModalDraggingResult = {
|
|
32
|
+
bind: () => DOMAttributes<HTMLElement>;
|
|
33
|
+
dragStyles: DragStyles;
|
|
34
|
+
};
|
|
35
|
+
declare const useModalDragging: (modalId: ModalElement["id"] | undefined, dragDirection?: DragDirection, cardRef?: RefObject<HTMLElement | null>, dragDismissedRef?: RefObject<boolean>, onFlyOutRest?: () => void) => UseModalDraggingResult;
|
|
36
|
+
|
|
37
|
+
type DragContextValue = {
|
|
38
|
+
bind: () => DOMAttributes<HTMLElement>;
|
|
39
|
+
active: boolean;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Returns the drag bind function for attaching drag-to-close to a handle
|
|
43
|
+
* element within a SpringModal.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const { bind } = useModalDrag()
|
|
47
|
+
* return <div {...bind()}>Drag handle</div>
|
|
48
|
+
*/
|
|
49
|
+
declare function useModalDrag(): DragContextValue;
|
|
50
|
+
/**
|
|
51
|
+
* A wrapper component that applies drag-to-close behavior to its children.
|
|
52
|
+
* Must be rendered inside SpringModal's children.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* <SpringModal>
|
|
56
|
+
* <DragHandle>
|
|
57
|
+
* <Header>Title</Header>
|
|
58
|
+
* </DragHandle>
|
|
59
|
+
* </SpringModal>
|
|
60
|
+
*/
|
|
61
|
+
declare const DragHandle: ({ children, className, enabled, }: {
|
|
62
|
+
children?: React.ReactNode;
|
|
63
|
+
className?: string;
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
66
|
+
|
|
67
|
+
type ModalProps = {
|
|
68
|
+
"aria-label"?: string;
|
|
69
|
+
afterClose?: () => void;
|
|
70
|
+
afterOpen?: () => void;
|
|
71
|
+
alignX?: ModalProps$1["alignX"];
|
|
72
|
+
alignY?: ModalProps$1["alignY"];
|
|
73
|
+
children?: React.ReactNode;
|
|
74
|
+
containerScrolling?: boolean;
|
|
75
|
+
contentClassName?: string;
|
|
76
|
+
contentStyle?: CSSProperties;
|
|
77
|
+
/**
|
|
78
|
+
* Spring transition values for the enter/from/leave animation phases.
|
|
79
|
+
*
|
|
80
|
+
* This prop accepts an object, so define it outside the component (or with
|
|
81
|
+
* `useMemo`) to keep the reference stable across renders.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const transitions = { from: { y: '100%' }, enter: { y: '0%' }, leave: { y: '100%' } }
|
|
85
|
+
* <SpringModal contentTransitions={transitions} ... />
|
|
86
|
+
*/
|
|
87
|
+
contentTransitions?: Pick<UseTransitionProps, "from" | "enter" | "leave">;
|
|
88
|
+
disableOpacityTransition?: boolean;
|
|
89
|
+
dismissible?: boolean;
|
|
90
|
+
dragDirection?: DragDirection;
|
|
91
|
+
/**
|
|
92
|
+
* Whether the modal's layer is currently active. Pass a value from
|
|
93
|
+
* `useLayerState` when using `@slithy/layers` for full layer-stack
|
|
94
|
+
* coordination. Defaults to `true`.
|
|
95
|
+
*/
|
|
96
|
+
layerIsActive?: boolean;
|
|
97
|
+
springConfig?: SpringConfig;
|
|
98
|
+
};
|
|
99
|
+
declare const Modal: ({ "aria-label": ariaLabel, afterClose, afterOpen, alignX, alignY, children, containerScrolling, contentClassName, contentStyle, contentTransitions, disableOpacityTransition, dismissible, dragDirection, layerIsActive: layerIsActiveProp, springConfig, }: ModalProps) => JSX.Element;
|
|
100
|
+
|
|
101
|
+
type ModalRendererProps = Pick<ModalRendererProps$1, "renderLayer" | "renderPortal"> & {
|
|
102
|
+
/**
|
|
103
|
+
* @deprecated `ModalRenderer` manages its own animated backdrop internally.
|
|
104
|
+
* This prop is accepted for interface parity with `@slithy/modal-kit`'s
|
|
105
|
+
* `ModalRenderer` but has no effect.
|
|
106
|
+
*/
|
|
107
|
+
renderBackdrop?: ModalRendererProps$1["renderBackdrop"];
|
|
108
|
+
};
|
|
109
|
+
declare const ModalRenderer: ({ renderLayer, renderPortal, }?: ModalRendererProps) => react_jsx_runtime.JSX.Element;
|
|
110
|
+
|
|
111
|
+
export { type DragDirection, DragHandle, type DragStyles, Modal, ModalBackdrop, type ModalProps, ModalRenderer, defaultSpring, iosSheetSpring, useModalDrag, useModalDragging };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
// src/animated.tsx
|
|
2
|
+
import { animated } from "@react-spring/web";
|
|
3
|
+
import { ModalBackdrop, ModalContent, ModalDialog } from "@slithy/modal-kit";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
var DivBase = forwardRef(
|
|
7
|
+
(props, ref) => /* @__PURE__ */ jsx("div", { ref, ...props })
|
|
8
|
+
);
|
|
9
|
+
var AnimatedDiv = animated(DivBase);
|
|
10
|
+
var AnimatedBackdrop = animated(ModalBackdrop);
|
|
11
|
+
var AnimatedModalContent = animated(ModalContent);
|
|
12
|
+
var ModalDialogBase = forwardRef(
|
|
13
|
+
(props, ref) => /* @__PURE__ */ jsx(ModalDialog, { ref, ...props })
|
|
14
|
+
);
|
|
15
|
+
var AnimatedModalDialog = animated(ModalDialogBase);
|
|
16
|
+
|
|
17
|
+
// src/ModalBackdrop.tsx
|
|
18
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
19
|
+
var ModalBackdrop2 = ({
|
|
20
|
+
className,
|
|
21
|
+
onClick,
|
|
22
|
+
style,
|
|
23
|
+
styles
|
|
24
|
+
}) => /* @__PURE__ */ jsx2(
|
|
25
|
+
AnimatedBackdrop,
|
|
26
|
+
{
|
|
27
|
+
className,
|
|
28
|
+
onClick,
|
|
29
|
+
style: { ...style, ...styles }
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// src/spring-configs.ts
|
|
34
|
+
function cubicBezier(x1, y1, x2, y2) {
|
|
35
|
+
const cx = 3 * x1;
|
|
36
|
+
const bx = 3 * (x2 - x1) - cx;
|
|
37
|
+
const ax = 1 - cx - bx;
|
|
38
|
+
const cy = 3 * y1;
|
|
39
|
+
const by = 3 * (y2 - y1) - cy;
|
|
40
|
+
const ay = 1 - cy - by;
|
|
41
|
+
const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
|
|
42
|
+
const sampleY = (t) => ((ay * t + by) * t + cy) * t;
|
|
43
|
+
const sampleDerivativeX = (t) => (3 * ax * t + 2 * bx) * t + cx;
|
|
44
|
+
const solve = (x) => {
|
|
45
|
+
let t = x;
|
|
46
|
+
for (let i = 0; i < 8; i++) {
|
|
47
|
+
const dx = sampleX(t) - x;
|
|
48
|
+
if (Math.abs(dx) < 1e-7) return t;
|
|
49
|
+
const d = sampleDerivativeX(t);
|
|
50
|
+
if (Math.abs(d) < 1e-6) break;
|
|
51
|
+
t -= dx / d;
|
|
52
|
+
}
|
|
53
|
+
return t;
|
|
54
|
+
};
|
|
55
|
+
return (x) => sampleY(solve(x));
|
|
56
|
+
}
|
|
57
|
+
var defaultSpring = {
|
|
58
|
+
friction: 200,
|
|
59
|
+
mass: 5,
|
|
60
|
+
tension: 2e3
|
|
61
|
+
};
|
|
62
|
+
var iosSheetSpring = {
|
|
63
|
+
duration: 500,
|
|
64
|
+
easing: cubicBezier(0.32, 0.72, 0, 1)
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/DragContext.tsx
|
|
68
|
+
import { createContext, useContext, useMemo as useMemo2 } from "react";
|
|
69
|
+
|
|
70
|
+
// src/useModalDragging.ts
|
|
71
|
+
import { useMemo, useRef } from "react";
|
|
72
|
+
import { useSpring } from "@react-spring/web";
|
|
73
|
+
import { useDrag } from "@use-gesture/react";
|
|
74
|
+
import { useModalStore } from "@slithy/modal-core";
|
|
75
|
+
var snapBackSpring = { friction: 40, tension: 300, mass: 1 };
|
|
76
|
+
var flyOutSpring = { friction: 20, tension: 150, mass: 1, clamp: true };
|
|
77
|
+
var noopModalDragging = {
|
|
78
|
+
bind: () => ({}),
|
|
79
|
+
dragStyles: {}
|
|
80
|
+
};
|
|
81
|
+
var useModalDragging = (modalId, dragDirection = "down", cardRef, dragDismissedRef, onFlyOutRest) => {
|
|
82
|
+
const closeModal = useModalStore((state) => state.closeModal);
|
|
83
|
+
const isXAxis = dragDirection === "left" || dragDirection === "right";
|
|
84
|
+
const axis = isXAxis ? "x" : "y";
|
|
85
|
+
const bounds = useMemo(
|
|
86
|
+
() => dragDirection === "down" ? { top: 0 } : dragDirection === "up" ? { bottom: 0 } : dragDirection === "right" ? { left: 0 } : { right: 0 },
|
|
87
|
+
[dragDirection]
|
|
88
|
+
);
|
|
89
|
+
const [dragStyles, api] = useSpring(() => ({
|
|
90
|
+
config: snapBackSpring,
|
|
91
|
+
x: 0,
|
|
92
|
+
y: 0
|
|
93
|
+
}));
|
|
94
|
+
const multiTouchCancelledRef = useRef(false);
|
|
95
|
+
const bind = useDrag(
|
|
96
|
+
({ down, touches, offset: [offsetX, offsetY], velocity: [velocityX, velocityY], direction: [dirX, dirY] }) => {
|
|
97
|
+
if (!modalId) return;
|
|
98
|
+
if (touches > 1) {
|
|
99
|
+
multiTouchCancelledRef.current = true;
|
|
100
|
+
api.start({ config: snapBackSpring, immediate: true, x: 0, y: 0 });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (multiTouchCancelledRef.current) {
|
|
104
|
+
if (!down) multiTouchCancelledRef.current = false;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const offset = isXAxis ? offsetX : offsetY;
|
|
108
|
+
const velocity = isXAxis ? velocityX : velocityY;
|
|
109
|
+
const dir = isXAxis ? dirX : dirY;
|
|
110
|
+
const isOutward = offset > 0 ? dragDirection === "down" || dragDirection === "right" : offset < 0 ? dragDirection === "up" || dragDirection === "left" : false;
|
|
111
|
+
const isMovingOutward = dragDirection === "down" ? dir > 0 : dragDirection === "up" ? dir < 0 : dragDirection === "right" ? dir > 0 : dir < 0;
|
|
112
|
+
const cardDimension = isXAxis ? cardRef?.current?.offsetWidth ?? 0 : cardRef?.current?.offsetHeight ?? 0;
|
|
113
|
+
const isDismissing = !down && isOutward && (isMovingOutward && velocity >= 0.5 || cardDimension > 0 && Math.abs(offset) >= cardDimension * 0.5);
|
|
114
|
+
if (isDismissing) {
|
|
115
|
+
if (dragDismissedRef) dragDismissedRef.current = true;
|
|
116
|
+
closeModal(modalId);
|
|
117
|
+
api.start({
|
|
118
|
+
config: flyOutSpring,
|
|
119
|
+
x: dragDirection === "right" ? window.innerWidth : dragDirection === "left" ? -window.innerWidth : 0,
|
|
120
|
+
y: dragDirection === "down" ? window.innerHeight : dragDirection === "up" ? -window.innerHeight : 0,
|
|
121
|
+
onRest: onFlyOutRest
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
api.start({
|
|
125
|
+
config: snapBackSpring,
|
|
126
|
+
immediate: down,
|
|
127
|
+
x: isXAxis ? down ? offsetX : 0 : 0,
|
|
128
|
+
y: !isXAxis ? down ? offsetY : 0 : 0
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{ axis, bounds, filterTaps: true, from: () => [dragStyles.x.get(), dragStyles.y.get()], rubberband: 0 }
|
|
133
|
+
);
|
|
134
|
+
return useMemo(
|
|
135
|
+
() => modalId ? { bind, dragStyles } : noopModalDragging,
|
|
136
|
+
[modalId, bind, dragStyles]
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// #style-inject:#style-inject
|
|
141
|
+
function styleInject(css, { insertAt } = {}) {
|
|
142
|
+
if (!css || typeof document === "undefined") return;
|
|
143
|
+
const head = document.head || document.getElementsByTagName("head")[0];
|
|
144
|
+
const style = document.createElement("style");
|
|
145
|
+
style.type = "text/css";
|
|
146
|
+
if (insertAt === "top") {
|
|
147
|
+
if (head.firstChild) {
|
|
148
|
+
head.insertBefore(style, head.firstChild);
|
|
149
|
+
} else {
|
|
150
|
+
head.appendChild(style);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
head.appendChild(style);
|
|
154
|
+
}
|
|
155
|
+
if (style.styleSheet) {
|
|
156
|
+
style.styleSheet.cssText = css;
|
|
157
|
+
} else {
|
|
158
|
+
style.appendChild(document.createTextNode(css));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/DragHandle.css
|
|
163
|
+
styleInject("[data-slithy=drag-handle][data-enabled] {\n cursor: grab;\n user-select: none;\n -webkit-user-select: none;\n}\n");
|
|
164
|
+
|
|
165
|
+
// src/DragContext.tsx
|
|
166
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
167
|
+
var DragContext = createContext({ bind: () => ({}), active: false });
|
|
168
|
+
var ModalDraggingSlot = ({
|
|
169
|
+
modalId,
|
|
170
|
+
dragDirection,
|
|
171
|
+
cardRef,
|
|
172
|
+
dragDismissedRef,
|
|
173
|
+
onFlyOutRest,
|
|
174
|
+
children
|
|
175
|
+
}) => {
|
|
176
|
+
const { bind, dragStyles } = useModalDragging(modalId, dragDirection, cardRef, dragDismissedRef, onFlyOutRest);
|
|
177
|
+
const contextValue = useMemo2(() => ({ bind, active: true }), [bind]);
|
|
178
|
+
return /* @__PURE__ */ jsx3(DragContext.Provider, { value: contextValue, children: children({ dragStyles }) });
|
|
179
|
+
};
|
|
180
|
+
function useModalDrag() {
|
|
181
|
+
return useContext(DragContext);
|
|
182
|
+
}
|
|
183
|
+
var DragHandle = ({
|
|
184
|
+
children,
|
|
185
|
+
className,
|
|
186
|
+
enabled = true
|
|
187
|
+
}) => {
|
|
188
|
+
const { bind, active } = useModalDrag();
|
|
189
|
+
const isActive = enabled && active;
|
|
190
|
+
return /* @__PURE__ */ jsx3(
|
|
191
|
+
"div",
|
|
192
|
+
{
|
|
193
|
+
className,
|
|
194
|
+
"data-slithy": "drag-handle",
|
|
195
|
+
"data-enabled": enabled || void 0,
|
|
196
|
+
...isActive ? bind() : {},
|
|
197
|
+
children
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// ../../node_modules/.pnpm/@react-spring+rafz@9.7.5/node_modules/@react-spring/rafz/dist/react-spring_rafz.modern.mjs
|
|
203
|
+
var updateQueue = makeQueue();
|
|
204
|
+
var raf = (fn) => schedule(fn, updateQueue);
|
|
205
|
+
var writeQueue = makeQueue();
|
|
206
|
+
raf.write = (fn) => schedule(fn, writeQueue);
|
|
207
|
+
var onStartQueue = makeQueue();
|
|
208
|
+
raf.onStart = (fn) => schedule(fn, onStartQueue);
|
|
209
|
+
var onFrameQueue = makeQueue();
|
|
210
|
+
raf.onFrame = (fn) => schedule(fn, onFrameQueue);
|
|
211
|
+
var onFinishQueue = makeQueue();
|
|
212
|
+
raf.onFinish = (fn) => schedule(fn, onFinishQueue);
|
|
213
|
+
var timeouts = [];
|
|
214
|
+
raf.setTimeout = (handler, ms) => {
|
|
215
|
+
const time = raf.now() + ms;
|
|
216
|
+
const cancel = () => {
|
|
217
|
+
const i = timeouts.findIndex((t) => t.cancel == cancel);
|
|
218
|
+
if (~i)
|
|
219
|
+
timeouts.splice(i, 1);
|
|
220
|
+
pendingCount -= ~i ? 1 : 0;
|
|
221
|
+
};
|
|
222
|
+
const timeout = { time, handler, cancel };
|
|
223
|
+
timeouts.splice(findTimeout(time), 0, timeout);
|
|
224
|
+
pendingCount += 1;
|
|
225
|
+
start();
|
|
226
|
+
return timeout;
|
|
227
|
+
};
|
|
228
|
+
var findTimeout = (time) => ~(~timeouts.findIndex((t) => t.time > time) || ~timeouts.length);
|
|
229
|
+
raf.cancel = (fn) => {
|
|
230
|
+
onStartQueue.delete(fn);
|
|
231
|
+
onFrameQueue.delete(fn);
|
|
232
|
+
onFinishQueue.delete(fn);
|
|
233
|
+
updateQueue.delete(fn);
|
|
234
|
+
writeQueue.delete(fn);
|
|
235
|
+
};
|
|
236
|
+
raf.sync = (fn) => {
|
|
237
|
+
sync = true;
|
|
238
|
+
raf.batchedUpdates(fn);
|
|
239
|
+
sync = false;
|
|
240
|
+
};
|
|
241
|
+
raf.throttle = (fn) => {
|
|
242
|
+
let lastArgs;
|
|
243
|
+
function queuedFn() {
|
|
244
|
+
try {
|
|
245
|
+
fn(...lastArgs);
|
|
246
|
+
} finally {
|
|
247
|
+
lastArgs = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function throttled(...args) {
|
|
251
|
+
lastArgs = args;
|
|
252
|
+
raf.onStart(queuedFn);
|
|
253
|
+
}
|
|
254
|
+
throttled.handler = fn;
|
|
255
|
+
throttled.cancel = () => {
|
|
256
|
+
onStartQueue.delete(queuedFn);
|
|
257
|
+
lastArgs = null;
|
|
258
|
+
};
|
|
259
|
+
return throttled;
|
|
260
|
+
};
|
|
261
|
+
var nativeRaf = typeof window != "undefined" ? window.requestAnimationFrame : (
|
|
262
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
263
|
+
(() => {
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
raf.use = (impl) => nativeRaf = impl;
|
|
267
|
+
raf.now = typeof performance != "undefined" ? () => performance.now() : Date.now;
|
|
268
|
+
raf.batchedUpdates = (fn) => fn();
|
|
269
|
+
raf.catch = console.error;
|
|
270
|
+
raf.frameLoop = "always";
|
|
271
|
+
raf.advance = () => {
|
|
272
|
+
if (raf.frameLoop !== "demand") {
|
|
273
|
+
console.warn(
|
|
274
|
+
"Cannot call the manual advancement of rafz whilst frameLoop is not set as demand"
|
|
275
|
+
);
|
|
276
|
+
} else {
|
|
277
|
+
update();
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var ts = -1;
|
|
281
|
+
var pendingCount = 0;
|
|
282
|
+
var sync = false;
|
|
283
|
+
function schedule(fn, queue) {
|
|
284
|
+
if (sync) {
|
|
285
|
+
queue.delete(fn);
|
|
286
|
+
fn(0);
|
|
287
|
+
} else {
|
|
288
|
+
queue.add(fn);
|
|
289
|
+
start();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function start() {
|
|
293
|
+
if (ts < 0) {
|
|
294
|
+
ts = 0;
|
|
295
|
+
if (raf.frameLoop !== "demand") {
|
|
296
|
+
nativeRaf(loop);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function stop() {
|
|
301
|
+
ts = -1;
|
|
302
|
+
}
|
|
303
|
+
function loop() {
|
|
304
|
+
if (~ts) {
|
|
305
|
+
nativeRaf(loop);
|
|
306
|
+
raf.batchedUpdates(update);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function update() {
|
|
310
|
+
const prevTs = ts;
|
|
311
|
+
ts = raf.now();
|
|
312
|
+
const count = findTimeout(ts);
|
|
313
|
+
if (count) {
|
|
314
|
+
eachSafely(timeouts.splice(0, count), (t) => t.handler());
|
|
315
|
+
pendingCount -= count;
|
|
316
|
+
}
|
|
317
|
+
if (!pendingCount) {
|
|
318
|
+
stop();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
onStartQueue.flush();
|
|
322
|
+
updateQueue.flush(prevTs ? Math.min(64, ts - prevTs) : 16.667);
|
|
323
|
+
onFrameQueue.flush();
|
|
324
|
+
writeQueue.flush();
|
|
325
|
+
onFinishQueue.flush();
|
|
326
|
+
}
|
|
327
|
+
function makeQueue() {
|
|
328
|
+
let next = /* @__PURE__ */ new Set();
|
|
329
|
+
let current = next;
|
|
330
|
+
return {
|
|
331
|
+
add(fn) {
|
|
332
|
+
pendingCount += current == next && !next.has(fn) ? 1 : 0;
|
|
333
|
+
next.add(fn);
|
|
334
|
+
},
|
|
335
|
+
delete(fn) {
|
|
336
|
+
pendingCount -= current == next && next.has(fn) ? 1 : 0;
|
|
337
|
+
return next.delete(fn);
|
|
338
|
+
},
|
|
339
|
+
flush(arg) {
|
|
340
|
+
if (current.size) {
|
|
341
|
+
next = /* @__PURE__ */ new Set();
|
|
342
|
+
pendingCount -= current.size;
|
|
343
|
+
eachSafely(current, (fn) => fn(arg) && next.add(fn));
|
|
344
|
+
pendingCount += next.size;
|
|
345
|
+
current = next;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function eachSafely(values, each) {
|
|
351
|
+
values.forEach((value) => {
|
|
352
|
+
try {
|
|
353
|
+
each(value);
|
|
354
|
+
} catch (e) {
|
|
355
|
+
raf.catch(e);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/Modal.tsx
|
|
361
|
+
import { useTransition as useTransition2 } from "@react-spring/web";
|
|
362
|
+
import { useCallback, useRef as useRef2, useState } from "react";
|
|
363
|
+
import {
|
|
364
|
+
ModalContainer,
|
|
365
|
+
useDialogKeyDown,
|
|
366
|
+
useModalLogic
|
|
367
|
+
} from "@slithy/modal-kit";
|
|
368
|
+
import { useMountEffect, useTrapFocus } from "@slithy/utils";
|
|
369
|
+
|
|
370
|
+
// src/Veil.tsx
|
|
371
|
+
import { useTransition } from "@react-spring/web";
|
|
372
|
+
import { useModalStore as useModalStore2 } from "@slithy/modal-core";
|
|
373
|
+
|
|
374
|
+
// src/Veil.css
|
|
375
|
+
styleInject("[data-slithy=modal-veil] {\n background: rgba(0, 0, 0, 0.25);\n inset: 0;\n position: absolute;\n user-select: none;\n -webkit-user-select: none;\n z-index: 100;\n}\n");
|
|
376
|
+
|
|
377
|
+
// src/Veil.tsx
|
|
378
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
379
|
+
var opaque = 1;
|
|
380
|
+
var transparent = 0;
|
|
381
|
+
var veilTransitions = {
|
|
382
|
+
config: defaultSpring,
|
|
383
|
+
from: { opacity: transparent },
|
|
384
|
+
enter: { opacity: opaque },
|
|
385
|
+
leave: { opacity: transparent }
|
|
386
|
+
};
|
|
387
|
+
var Veil = ({ modalId, skipAnimation }) => {
|
|
388
|
+
const { backdropId, topModalId } = useModalStore2(
|
|
389
|
+
({ backdropId: backdropId2, topModalId: topModalId2 }) => ({ backdropId: backdropId2, topModalId: topModalId2 })
|
|
390
|
+
);
|
|
391
|
+
const transitions = useTransition(
|
|
392
|
+
backdropId !== modalId && topModalId !== modalId,
|
|
393
|
+
{
|
|
394
|
+
...veilTransitions,
|
|
395
|
+
immediate: !!skipAnimation
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
return transitions(
|
|
399
|
+
(springStyles, show) => show ? /* @__PURE__ */ jsx4(
|
|
400
|
+
AnimatedDiv,
|
|
401
|
+
{
|
|
402
|
+
"data-slithy": "modal-veil",
|
|
403
|
+
"data-testid": "modal-veil",
|
|
404
|
+
style: springStyles
|
|
405
|
+
}
|
|
406
|
+
) : null
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// src/Modal.tsx
|
|
411
|
+
import { jsx as jsx5, jsxs } from "react/jsx-runtime";
|
|
412
|
+
var opaque2 = 1;
|
|
413
|
+
var transparent2 = 0;
|
|
414
|
+
var Modal = ({
|
|
415
|
+
"aria-label": ariaLabel,
|
|
416
|
+
afterClose,
|
|
417
|
+
afterOpen,
|
|
418
|
+
alignX = "center",
|
|
419
|
+
alignY = "middle",
|
|
420
|
+
children,
|
|
421
|
+
containerScrolling = true,
|
|
422
|
+
contentClassName,
|
|
423
|
+
contentStyle,
|
|
424
|
+
contentTransitions,
|
|
425
|
+
disableOpacityTransition,
|
|
426
|
+
dismissible = true,
|
|
427
|
+
dragDirection,
|
|
428
|
+
layerIsActive: layerIsActiveProp,
|
|
429
|
+
springConfig
|
|
430
|
+
}) => {
|
|
431
|
+
const trapFocus = useTrapFocus();
|
|
432
|
+
const [springOpen, setSpringOpen] = useState(false);
|
|
433
|
+
useMountEffect(() => {
|
|
434
|
+
const cb = () => setSpringOpen(true);
|
|
435
|
+
raf(cb);
|
|
436
|
+
return () => raf.cancel(cb);
|
|
437
|
+
});
|
|
438
|
+
const [scrollable, setScrollable] = useState(false);
|
|
439
|
+
const doneRef = useRef2(null);
|
|
440
|
+
const dragDismissedRef = useRef2(false);
|
|
441
|
+
const onLeave = useCallback((done) => {
|
|
442
|
+
doneRef.current = done;
|
|
443
|
+
setScrollable(false);
|
|
444
|
+
setSpringOpen(false);
|
|
445
|
+
}, []);
|
|
446
|
+
const {
|
|
447
|
+
contentRef,
|
|
448
|
+
handleCloseModal,
|
|
449
|
+
layerIsActive,
|
|
450
|
+
skipAnimation,
|
|
451
|
+
isTopModal,
|
|
452
|
+
markAtRest,
|
|
453
|
+
modalId,
|
|
454
|
+
modalState
|
|
455
|
+
} = useModalLogic({
|
|
456
|
+
afterClose,
|
|
457
|
+
// afterOpen is fired from enter.onRest instead of the mount effect
|
|
458
|
+
afterOpen: void 0,
|
|
459
|
+
delayAtRest: true,
|
|
460
|
+
layerIsActive: layerIsActiveProp,
|
|
461
|
+
onLeave
|
|
462
|
+
});
|
|
463
|
+
const onKeyDown = useDialogKeyDown({
|
|
464
|
+
dismissible,
|
|
465
|
+
handleCloseModal,
|
|
466
|
+
isTopModal,
|
|
467
|
+
layerIsActive,
|
|
468
|
+
onKeyDown: trapFocus.onKeyDown
|
|
469
|
+
});
|
|
470
|
+
const transitions = useTransition2(springOpen, {
|
|
471
|
+
config: springConfig ?? defaultSpring,
|
|
472
|
+
from: { opacity: transparent2, ...contentTransitions?.from },
|
|
473
|
+
enter: {
|
|
474
|
+
opacity: opaque2,
|
|
475
|
+
...contentTransitions?.enter,
|
|
476
|
+
onRest: () => {
|
|
477
|
+
if (springOpen) {
|
|
478
|
+
markAtRest();
|
|
479
|
+
afterOpen?.();
|
|
480
|
+
if (containerScrolling) setScrollable(true);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
leave: {
|
|
485
|
+
opacity: transparent2,
|
|
486
|
+
...dragDismissedRef.current ? { immediate: true } : contentTransitions?.leave,
|
|
487
|
+
onRest: () => {
|
|
488
|
+
if (!springOpen && doneRef.current) {
|
|
489
|
+
if (dragDismissedRef.current) return;
|
|
490
|
+
doneRef.current();
|
|
491
|
+
doneRef.current = null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
immediate: !!skipAnimation
|
|
496
|
+
});
|
|
497
|
+
const renderContent = (dragStyles, styles) => /* @__PURE__ */ jsx5(
|
|
498
|
+
ModalContainer,
|
|
499
|
+
{
|
|
500
|
+
alignX,
|
|
501
|
+
modalId,
|
|
502
|
+
onBackdropClick: dismissible ? handleCloseModal : void 0,
|
|
503
|
+
scrollable,
|
|
504
|
+
children: /* @__PURE__ */ jsx5(
|
|
505
|
+
AnimatedModalContent,
|
|
506
|
+
{
|
|
507
|
+
alignY,
|
|
508
|
+
disableOpacityTransition,
|
|
509
|
+
modalState,
|
|
510
|
+
style: styles,
|
|
511
|
+
children: /* @__PURE__ */ jsxs(
|
|
512
|
+
AnimatedModalDialog,
|
|
513
|
+
{
|
|
514
|
+
ref: contentRef,
|
|
515
|
+
"aria-label": ariaLabel,
|
|
516
|
+
className: contentClassName,
|
|
517
|
+
onKeyDown,
|
|
518
|
+
style: {
|
|
519
|
+
...contentStyle,
|
|
520
|
+
x: dragStyles.x ?? 0,
|
|
521
|
+
y: dragStyles.y ?? 0
|
|
522
|
+
},
|
|
523
|
+
children: [
|
|
524
|
+
children,
|
|
525
|
+
/* @__PURE__ */ jsx5(Veil, { modalId, skipAnimation })
|
|
526
|
+
]
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
return transitions(
|
|
534
|
+
(styles, show) => show ? dismissible ? /* @__PURE__ */ jsx5(
|
|
535
|
+
ModalDraggingSlot,
|
|
536
|
+
{
|
|
537
|
+
modalId,
|
|
538
|
+
dragDirection,
|
|
539
|
+
cardRef: contentRef,
|
|
540
|
+
dragDismissedRef,
|
|
541
|
+
onFlyOutRest: () => {
|
|
542
|
+
if (doneRef.current) {
|
|
543
|
+
doneRef.current();
|
|
544
|
+
doneRef.current = null;
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
children: ({ dragStyles }) => renderContent(dragStyles, styles)
|
|
548
|
+
}
|
|
549
|
+
) : renderContent({}, styles) : null
|
|
550
|
+
);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// src/ModalRenderer.tsx
|
|
554
|
+
import { useTransition as useTransition3 } from "@react-spring/web";
|
|
555
|
+
import { useModalStore as useModalStore3 } from "@slithy/modal-core";
|
|
556
|
+
import { ModalRenderer as ModalKitRenderer } from "@slithy/modal-kit";
|
|
557
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
558
|
+
var opaque3 = 1;
|
|
559
|
+
var transparent3 = 0;
|
|
560
|
+
var backdropTransitions = {
|
|
561
|
+
config: defaultSpring,
|
|
562
|
+
from: { opacity: transparent3 },
|
|
563
|
+
enter: { opacity: opaque3 },
|
|
564
|
+
leave: { opacity: transparent3 }
|
|
565
|
+
};
|
|
566
|
+
var ModalRenderer = ({
|
|
567
|
+
renderLayer,
|
|
568
|
+
renderPortal
|
|
569
|
+
} = {}) => {
|
|
570
|
+
const { backdropId, modals } = useModalStore3(({ backdropId: backdropId2, modals: modals2 }) => ({
|
|
571
|
+
backdropId: backdropId2,
|
|
572
|
+
modals: modals2
|
|
573
|
+
}));
|
|
574
|
+
const modalsExist = modals.length > 0;
|
|
575
|
+
const transitions = useTransition3(!!backdropId && modalsExist, {
|
|
576
|
+
...backdropTransitions,
|
|
577
|
+
immediate: !!modals.at(-1)?.skipAnimation
|
|
578
|
+
});
|
|
579
|
+
const renderBackdrop = () => transitions(
|
|
580
|
+
(styles, show) => show ? /* @__PURE__ */ jsx6(ModalBackdrop2, { styles }) : null
|
|
581
|
+
);
|
|
582
|
+
return /* @__PURE__ */ jsx6(
|
|
583
|
+
ModalKitRenderer,
|
|
584
|
+
{
|
|
585
|
+
renderBackdrop,
|
|
586
|
+
renderLayer,
|
|
587
|
+
renderPortal
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
};
|
|
591
|
+
export {
|
|
592
|
+
DragHandle,
|
|
593
|
+
Modal,
|
|
594
|
+
ModalBackdrop2 as ModalBackdrop,
|
|
595
|
+
ModalRenderer,
|
|
596
|
+
defaultSpring,
|
|
597
|
+
iosSheetSpring,
|
|
598
|
+
useModalDrag,
|
|
599
|
+
useModalDragging
|
|
600
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@slithy/modal-spring",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "React Spring animation adapter for @slithy/modal-kit.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"sideEffects": [
|
|
16
|
+
"*.css"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@slithy/modal-kit": "0.1.2",
|
|
20
|
+
"@slithy/modal-core": "0.1.2",
|
|
21
|
+
"@slithy/utils": "0.3.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@react-spring/web": ">=9",
|
|
25
|
+
"@use-gesture/react": ">=10",
|
|
26
|
+
"react": "^17 || ^18 || ^19"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@react-spring/rafz": "^9",
|
|
30
|
+
"@react-spring/web": "^9",
|
|
31
|
+
"@use-gesture/react": "^10.3.1",
|
|
32
|
+
"@testing-library/jest-dom": "^6",
|
|
33
|
+
"@testing-library/react": "^16",
|
|
34
|
+
"@types/react": "^19",
|
|
35
|
+
"@vitejs/plugin-react": "^6",
|
|
36
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
37
|
+
"jsdom": "^29.0.1",
|
|
38
|
+
"react": "^19",
|
|
39
|
+
"react-dom": "^19",
|
|
40
|
+
"tsup": "^8",
|
|
41
|
+
"typescript": "^5",
|
|
42
|
+
"vitest": "^4.1.2",
|
|
43
|
+
"@slithy/tsconfig": "0.0.0",
|
|
44
|
+
"@slithy/eslint-config": "0.0.0"
|
|
45
|
+
},
|
|
46
|
+
"author": "mjcampagna",
|
|
47
|
+
"license": "ISC",
|
|
48
|
+
"scripts": {
|
|
49
|
+
"clean": "rm -rf dist",
|
|
50
|
+
"build": "rm -rf dist && tsup",
|
|
51
|
+
"dev": "tsup --watch",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"lint": "eslint .",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest"
|
|
56
|
+
}
|
|
57
|
+
}
|