reshaped 4.2.0 → 4.3.0-canary.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/dist/bundle.css +1 -1
- package/dist/bundle.js +2 -2
- package/dist/components/Autocomplete/Autocomplete.js +1 -1
- package/dist/components/ContextMenu/tests/ContextMenu.stories.d.ts +7 -0
- package/dist/components/ContextMenu/tests/ContextMenu.stories.js +99 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +1 -1
- package/dist/components/Flyout/Flyout.module.css +1 -1
- package/dist/components/Flyout/Flyout.types.d.ts +1 -0
- package/dist/components/Flyout/FlyoutContent.js +3 -2
- package/dist/components/Flyout/FlyoutControlled.js +23 -4
- package/dist/components/Flyout/tests/Flyout.stories.d.ts +7 -0
- package/dist/components/Flyout/tests/Flyout.stories.js +142 -1
- package/dist/components/Modal/Modal.js +13 -171
- package/dist/components/Modal/Modal.module.css +1 -1
- package/dist/components/Modal/Modal.types.d.ts +2 -1
- package/dist/components/Modal/useModalDrag.d.ts +21 -0
- package/dist/components/Modal/useModalDrag.js +256 -0
- package/dist/components/Overlay/Overlay.js +5 -4
- package/dist/components/Overlay/Overlay.module.css +1 -1
- package/dist/components/ScrollArea/ScrollArea.js +29 -16
- package/dist/components/ScrollArea/ScrollArea.module.css +1 -1
- package/dist/components/ScrollArea/ScrollArea.types.d.ts +1 -1
- package/dist/components/Select/SelectCustomControlled.js +1 -1
- package/dist/components/Select/tests/Select.stories.js +12 -1
- package/dist/components/_private/Freeze/Freeze.d.ts +4 -0
- package/dist/components/_private/Freeze/Freeze.js +46 -0
- package/dist/components/_private/Freeze/Freeze.module.css +1 -0
- package/dist/components/_private/Freeze/Freeze.types.d.ts +14 -0
- package/dist/components/_private/Freeze/Freeze.types.js +1 -0
- package/dist/components/_private/Freeze/index.d.ts +1 -0
- package/dist/components/_private/Freeze/index.js +1 -0
- package/dist/components/_private/Freeze/tests/Freeze.stories.d.ts +7 -0
- package/dist/components/_private/Freeze/tests/Freeze.stories.js +88 -0
- package/dist/hooks/_internal/useSingletonHotkeys.js +4 -0
- package/dist/hooks/tests/useHotkeys.stories.d.ts +3 -0
- package/dist/hooks/tests/useHotkeys.stories.js +18 -1
- package/dist/hooks/tests/useOnClickOutside.stories.d.ts +6 -0
- package/dist/hooks/tests/useOnClickOutside.stories.js +39 -1
- package/dist/hooks/useOnClickOutside.js +12 -12
- package/dist/utilities/animation.d.ts +17 -0
- package/dist/utilities/animation.js +73 -0
- package/package.json +3 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
export type Props = {
|
|
3
|
+
/** Stop committing updates to the children and keep their last rendered output on the screen */
|
|
4
|
+
frozen: boolean;
|
|
5
|
+
/** Node for inserting children */
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
};
|
|
8
|
+
export type State = {
|
|
9
|
+
/** Frozen render was committed, children stay suspended until unfrozen */
|
|
10
|
+
committed?: boolean;
|
|
11
|
+
/** Frozen render wasn't committed in time, children render without freezing */
|
|
12
|
+
expired?: boolean;
|
|
13
|
+
pending?: Promise<void>;
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Freeze";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Freeze";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.transition = exports.base = void 0;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
const test_1 = require("storybook/test");
|
|
9
|
+
const Freeze_1 = __importDefault(require("../../../../components/_private/Freeze"));
|
|
10
|
+
const Button_1 = __importDefault(require("../../../../components/Button"));
|
|
11
|
+
const View_1 = __importDefault(require("../../../../components/View"));
|
|
12
|
+
exports.default = { title: "Internal/Freeze" };
|
|
13
|
+
const ValueContext = react_1.default.createContext(0);
|
|
14
|
+
const ContextValue = () => {
|
|
15
|
+
const value = react_1.default.useContext(ValueContext);
|
|
16
|
+
return <div data-testid="context-value">Context: {value}</div>;
|
|
17
|
+
};
|
|
18
|
+
const Demo = (props) => {
|
|
19
|
+
const [frozen, setFrozen] = react_1.default.useState(false);
|
|
20
|
+
const [value, setValue] = react_1.default.useState(0);
|
|
21
|
+
const update = (nextFrozen) => {
|
|
22
|
+
const apply = () => {
|
|
23
|
+
setFrozen(nextFrozen);
|
|
24
|
+
setValue((prev) => prev + 1);
|
|
25
|
+
};
|
|
26
|
+
if (props.transition) {
|
|
27
|
+
react_1.default.startTransition(apply);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
apply();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return (<View_1.default gap={4}>
|
|
34
|
+
<View_1.default direction="row" gap={2}>
|
|
35
|
+
<Button_1.default onClick={() => update(true)}>Freeze and update</Button_1.default>
|
|
36
|
+
<Button_1.default onClick={() => update(false)}>Unfreeze and update</Button_1.default>
|
|
37
|
+
</View_1.default>
|
|
38
|
+
|
|
39
|
+
<div data-testid="frozen-state">{frozen ? "Frozen" : "Active"}</div>
|
|
40
|
+
|
|
41
|
+
<ValueContext.Provider value={value}>
|
|
42
|
+
<Freeze_1.default frozen={frozen}>
|
|
43
|
+
<div data-testid="prop-value">Prop: {value}</div>
|
|
44
|
+
<ContextValue />
|
|
45
|
+
</Freeze_1.default>
|
|
46
|
+
</ValueContext.Provider>
|
|
47
|
+
</View_1.default>);
|
|
48
|
+
};
|
|
49
|
+
exports.base = {
|
|
50
|
+
name: "base",
|
|
51
|
+
render: () => <Demo />,
|
|
52
|
+
play: async ({ canvasElement }) => {
|
|
53
|
+
const canvas = (0, test_1.within)(canvasElement);
|
|
54
|
+
const [freezeButton, unfreezeButton] = canvas.getAllByRole("button");
|
|
55
|
+
await test_1.userEvent.click(freezeButton);
|
|
56
|
+
// Updates coming in the same render as freezing are ignored
|
|
57
|
+
(0, test_1.expect)(canvas.getByTestId("frozen-state")).toHaveTextContent("Frozen");
|
|
58
|
+
(0, test_1.expect)(canvas.getByTestId("prop-value")).toHaveTextContent("Prop: 0");
|
|
59
|
+
(0, test_1.expect)(canvas.getByTestId("context-value")).toHaveTextContent("Context: 0");
|
|
60
|
+
// Frozen content stays visible even though it's suspended
|
|
61
|
+
(0, test_1.expect)(canvas.getByTestId("prop-value")).toBeVisible();
|
|
62
|
+
(0, test_1.expect)(canvas.getByTestId("context-value")).toBeVisible();
|
|
63
|
+
await test_1.userEvent.click(freezeButton);
|
|
64
|
+
(0, test_1.expect)(canvas.getByTestId("prop-value")).toHaveTextContent("Prop: 0");
|
|
65
|
+
await test_1.userEvent.click(unfreezeButton);
|
|
66
|
+
await (0, test_1.waitFor)(() => {
|
|
67
|
+
(0, test_1.expect)(canvas.getByTestId("frozen-state")).toHaveTextContent("Active");
|
|
68
|
+
(0, test_1.expect)(canvas.getByTestId("prop-value")).toHaveTextContent("Prop: 3");
|
|
69
|
+
(0, test_1.expect)(canvas.getByTestId("context-value")).toHaveTextContent("Context: 3");
|
|
70
|
+
});
|
|
71
|
+
(0, test_1.expect)(canvas.getByTestId("prop-value")).toBeVisible();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
exports.transition = {
|
|
75
|
+
name: "test: freezing in a transition",
|
|
76
|
+
render: () => <Demo transition/>,
|
|
77
|
+
play: async ({ canvasElement }) => {
|
|
78
|
+
const canvas = (0, test_1.within)(canvasElement);
|
|
79
|
+
const [freezeButton] = canvas.getAllByRole("button");
|
|
80
|
+
await test_1.userEvent.click(freezeButton);
|
|
81
|
+
// Transitions can't commit a frozen render, so they shouldn't be blocked by it
|
|
82
|
+
await (0, test_1.waitFor)(() => {
|
|
83
|
+
(0, test_1.expect)(canvas.getByTestId("frozen-state")).toHaveTextContent("Frozen");
|
|
84
|
+
(0, test_1.expect)(canvas.getByTestId("prop-value")).toHaveTextContent("Prop: 1");
|
|
85
|
+
});
|
|
86
|
+
(0, test_1.expect)(canvas.getByTestId("prop-value")).toBeVisible();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -173,6 +173,10 @@ const HotkeysProvider = (props) => {
|
|
|
173
173
|
// Browsers trigger keyboard event without passing e.key when you click on autocomplete
|
|
174
174
|
if (!e.key)
|
|
175
175
|
return;
|
|
176
|
+
// Keys handled by an IME are not hotkeys. Safari fires the Enter that confirms
|
|
177
|
+
// a composition after compositionend, so only keyCode 229 marks it
|
|
178
|
+
if (e.isComposing || e.keyCode === 229)
|
|
179
|
+
return;
|
|
176
180
|
const nextPressedMap = addPressedKey(e) ?? pressedMapRef.current;
|
|
177
181
|
globalHotkeyStore.handleKeyDown(nextPressedMap, e);
|
|
178
182
|
}, [addPressedKey]);
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.optionModified = exports.keyCombinationMoreThanRequired = exports.keyCombinationOrder = exports.keyCombinationFormat = exports.keyCombination = exports.keyList = exports.modKeyHold = exports.modKey = exports.singleKey = exports.base = void 0;
|
|
6
|
+
exports.imeComposition = exports.optionModified = exports.keyCombinationMoreThanRequired = exports.keyCombinationOrder = exports.keyCombinationFormat = exports.keyCombination = exports.keyList = exports.modKeyHold = exports.modKey = exports.singleKey = exports.base = void 0;
|
|
7
7
|
const test_1 = require("storybook/test");
|
|
8
8
|
const useHotkeys_1 = __importDefault(require("../useHotkeys"));
|
|
9
9
|
exports.default = {
|
|
@@ -178,3 +178,20 @@ exports.optionModified = {
|
|
|
178
178
|
(0, test_1.expect)(args.handleHotkey).toHaveBeenCalledTimes(1);
|
|
179
179
|
},
|
|
180
180
|
};
|
|
181
|
+
exports.imeComposition = {
|
|
182
|
+
name: "ignores IME composition",
|
|
183
|
+
args: {
|
|
184
|
+
handleHotkey: (0, test_1.fn)(),
|
|
185
|
+
},
|
|
186
|
+
render: (args) => <Component hotkeys={{ enter: args.handleHotkey }}/>,
|
|
187
|
+
play: async ({ args }) => {
|
|
188
|
+
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", isComposing: true }));
|
|
189
|
+
window.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter" }));
|
|
190
|
+
// Safari ends the composition before keydown, so only keyCode 229 marks it
|
|
191
|
+
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", keyCode: 229 }));
|
|
192
|
+
window.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter" }));
|
|
193
|
+
(0, test_1.expect)(args.handleHotkey).not.toHaveBeenCalled();
|
|
194
|
+
await test_1.userEvent.keyboard("{Enter}");
|
|
195
|
+
(0, test_1.expect)(args.handleHotkey).toHaveBeenCalledTimes(1);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
@@ -12,6 +12,12 @@ export default _default;
|
|
|
12
12
|
export declare const base: StoryObj<{
|
|
13
13
|
handleOutsideClick: Mock;
|
|
14
14
|
}>;
|
|
15
|
+
export declare const rightClick: StoryObj<{
|
|
16
|
+
handleOutsideClick: Mock;
|
|
17
|
+
}>;
|
|
18
|
+
export declare const keyboardContextMenu: StoryObj<{
|
|
19
|
+
handleOutsideClick: Mock;
|
|
20
|
+
}>;
|
|
15
21
|
export declare const refs: StoryObj<{
|
|
16
22
|
handleOutsideClick: Mock;
|
|
17
23
|
}>;
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.deps = exports.disabled = exports.refs = exports.base = void 0;
|
|
6
|
+
exports.deps = exports.disabled = exports.refs = exports.keyboardContextMenu = exports.rightClick = exports.base = void 0;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const test_1 = require("storybook/test");
|
|
9
9
|
const useOnClickOutside_1 = __importDefault(require("../useOnClickOutside"));
|
|
@@ -46,6 +46,44 @@ exports.base = {
|
|
|
46
46
|
(0, test_1.expect)(args.handleOutsideClick).toHaveBeenCalledWith();
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
|
+
exports.rightClick = {
|
|
50
|
+
name: "test: right click",
|
|
51
|
+
args: {
|
|
52
|
+
handleOutsideClick: (0, test_1.fn)(),
|
|
53
|
+
},
|
|
54
|
+
render: (args) => <DemoBase onOutsideClick={args.handleOutsideClick}/>,
|
|
55
|
+
play: async ({ canvas, args }) => {
|
|
56
|
+
const button = canvas.getAllByRole("button")[0];
|
|
57
|
+
await test_1.userEvent.pointer({ keys: "[MouseRight]", target: button });
|
|
58
|
+
(0, test_1.expect)(args.handleOutsideClick).not.toHaveBeenCalled();
|
|
59
|
+
await test_1.userEvent.pointer({ keys: "[MouseRight]", target: document.body });
|
|
60
|
+
(0, test_1.expect)(args.handleOutsideClick).toHaveBeenCalledTimes(1);
|
|
61
|
+
(0, test_1.expect)(args.handleOutsideClick).toHaveBeenCalledWith();
|
|
62
|
+
// Opening another context menu outside
|
|
63
|
+
const preventDefault = (e) => e.preventDefault();
|
|
64
|
+
document.body.addEventListener("contextmenu", preventDefault);
|
|
65
|
+
await test_1.userEvent.pointer({ keys: "[MouseRight]", target: document.body });
|
|
66
|
+
document.body.removeEventListener("contextmenu", preventDefault);
|
|
67
|
+
(0, test_1.expect)(args.handleOutsideClick).toHaveBeenCalledTimes(2);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
exports.keyboardContextMenu = {
|
|
71
|
+
name: "test: context menu key",
|
|
72
|
+
args: {
|
|
73
|
+
handleOutsideClick: (0, test_1.fn)(),
|
|
74
|
+
},
|
|
75
|
+
render: (args) => <DemoBase onOutsideClick={args.handleOutsideClick}/>,
|
|
76
|
+
play: async ({ canvas, args }) => {
|
|
77
|
+
const button = canvas.getAllByRole("button")[0];
|
|
78
|
+
// Context menu key doesn't trigger mousedown
|
|
79
|
+
test_1.fireEvent.mouseDown(document.body);
|
|
80
|
+
test_1.fireEvent.contextMenu(button);
|
|
81
|
+
(0, test_1.expect)(args.handleOutsideClick).not.toHaveBeenCalled();
|
|
82
|
+
test_1.fireEvent.mouseDown(button);
|
|
83
|
+
test_1.fireEvent.contextMenu(document.body);
|
|
84
|
+
(0, test_1.expect)(args.handleOutsideClick).toHaveBeenCalledTimes(1);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
49
87
|
const DemoRefs = (props) => {
|
|
50
88
|
const ref = react_1.default.useRef(null);
|
|
51
89
|
const ref2 = react_1.default.useRef(null);
|
|
@@ -45,25 +45,25 @@ const useOnClickOutside = (refs, handler, options) => {
|
|
|
45
45
|
if (disabled)
|
|
46
46
|
return;
|
|
47
47
|
const handleClick = (event) => {
|
|
48
|
-
if (event.type === "contextmenu") {
|
|
49
|
-
/**
|
|
50
|
-
* Context menu events can be claimed by a handler that opens a menu of its own,
|
|
51
|
-
* which prevents the default browser behavior. Such events open a new flyout
|
|
52
|
-
* instead of dismissing the current one, so they're not outside clicks
|
|
53
|
-
*/
|
|
54
|
-
if (event.defaultPrevented)
|
|
55
|
-
return;
|
|
56
|
-
isMouseDownInsideRef.current = checkEventInsideRefs(event);
|
|
57
|
-
}
|
|
58
48
|
if (isMouseDownInsideRef.current)
|
|
59
49
|
return;
|
|
60
50
|
handlerRef.current?.(event);
|
|
61
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Capture phase closes the content before another context menu opens on the same event,
|
|
54
|
+
* while the content opened by this event subscribes too late to get closed by it.
|
|
55
|
+
* Target is checked directly since the context menu key doesn't trigger mousedown
|
|
56
|
+
*/
|
|
57
|
+
const handleContextMenu = (event) => {
|
|
58
|
+
if (checkEventInsideRefs(event))
|
|
59
|
+
return;
|
|
60
|
+
handlerRef.current?.(event);
|
|
61
|
+
};
|
|
62
62
|
document.addEventListener("click", handleClick);
|
|
63
|
-
document.addEventListener("contextmenu",
|
|
63
|
+
document.addEventListener("contextmenu", handleContextMenu, { capture: true });
|
|
64
64
|
return () => {
|
|
65
65
|
document.removeEventListener("click", handleClick);
|
|
66
|
-
document.removeEventListener("contextmenu",
|
|
66
|
+
document.removeEventListener("contextmenu", handleContextMenu, { capture: true });
|
|
67
67
|
};
|
|
68
68
|
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
69
69
|
}, [handlerRef, disabled, ...refs]);
|
|
@@ -2,3 +2,20 @@ export declare const onNextFrame: (cb: () => void) => void;
|
|
|
2
2
|
export declare const disableTransitions: () => void;
|
|
3
3
|
export declare const enableTransitions: () => void;
|
|
4
4
|
export declare const checkTransitions: () => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a critically damped spring into a css transition duration and a linear() easing,
|
|
7
|
+
* so the release animation continues from the current velocity of the gesture instead of restarting from zero.
|
|
8
|
+
* Browsers without linear() easing support fall back to a decelerating transition scaled by the velocity.
|
|
9
|
+
*
|
|
10
|
+
* @param displacement - current position relative to the target position, in px
|
|
11
|
+
* @param velocity - current velocity in px/ms, measured on the same axis as the displacement
|
|
12
|
+
* and positive when moving away from the target position
|
|
13
|
+
* @returns transition duration in ms and its timing function
|
|
14
|
+
*/
|
|
15
|
+
export declare const resolveSpringTransition: (args: {
|
|
16
|
+
displacement: number;
|
|
17
|
+
velocity: number;
|
|
18
|
+
}) => {
|
|
19
|
+
duration: number;
|
|
20
|
+
easing: string;
|
|
21
|
+
};
|
|
@@ -18,3 +18,76 @@ export const checkTransitions = () => {
|
|
|
18
18
|
return false;
|
|
19
19
|
return !window.matchMedia(reducedMotionMediaQuery).matches;
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Angular frequency of the critically damped spring used for the gesture release animations, in 1/s.
|
|
23
|
+
* Higher values settle faster, 20 settles from a resting position in ~300ms
|
|
24
|
+
*/
|
|
25
|
+
const SPRING_OMEGA = 20;
|
|
26
|
+
const SPRING_SAMPLE_RATE = 120;
|
|
27
|
+
const SPRING_MAX_DURATION = 600;
|
|
28
|
+
const SPRING_MIN_DURATION = 50;
|
|
29
|
+
const SPRING_REST_DISTANCE = 0.5;
|
|
30
|
+
const SPRING_REST_VELOCITY = 20;
|
|
31
|
+
/** Velocity is capped so a fast flick doesn't resolve into an unreasonably long easing curve */
|
|
32
|
+
const SPRING_MAX_VELOCITY = 3;
|
|
33
|
+
/** Duration range used when the browser doesn't support linear() easing */
|
|
34
|
+
const FALLBACK_MIN_DURATION = 100;
|
|
35
|
+
const FALLBACK_MAX_DURATION = 300;
|
|
36
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
37
|
+
let supportsLinearEasing = null;
|
|
38
|
+
const checkLinearEasing = () => {
|
|
39
|
+
if (supportsLinearEasing !== null)
|
|
40
|
+
return supportsLinearEasing;
|
|
41
|
+
supportsLinearEasing =
|
|
42
|
+
typeof CSS !== "undefined" &&
|
|
43
|
+
typeof CSS.supports === "function" &&
|
|
44
|
+
CSS.supports("transition-timing-function", "linear(0, 1)");
|
|
45
|
+
return supportsLinearEasing;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Resolves a critically damped spring into a css transition duration and a linear() easing,
|
|
49
|
+
* so the release animation continues from the current velocity of the gesture instead of restarting from zero.
|
|
50
|
+
* Browsers without linear() easing support fall back to a decelerating transition scaled by the velocity.
|
|
51
|
+
*
|
|
52
|
+
* @param displacement - current position relative to the target position, in px
|
|
53
|
+
* @param velocity - current velocity in px/ms, measured on the same axis as the displacement
|
|
54
|
+
* and positive when moving away from the target position
|
|
55
|
+
* @returns transition duration in ms and its timing function
|
|
56
|
+
*/
|
|
57
|
+
export const resolveSpringTransition = (args) => {
|
|
58
|
+
const { displacement } = args;
|
|
59
|
+
const velocity = clamp(args.velocity, -SPRING_MAX_VELOCITY, SPRING_MAX_VELOCITY);
|
|
60
|
+
if (!displacement)
|
|
61
|
+
return { duration: 0, easing: "linear" };
|
|
62
|
+
if (!checkLinearEasing()) {
|
|
63
|
+
const duration = Math.abs(displacement) / Math.max(Math.abs(velocity), 0.001);
|
|
64
|
+
return {
|
|
65
|
+
duration: clamp(duration, FALLBACK_MIN_DURATION, FALLBACK_MAX_DURATION),
|
|
66
|
+
easing: "var(--rs-easing-decelerate)",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const step = 1000 / SPRING_SAMPLE_RATE;
|
|
70
|
+
// Position over time: (a + b * t) * e^(-omega * t), with the time in seconds
|
|
71
|
+
const a = displacement;
|
|
72
|
+
const b = velocity * 1000 + SPRING_OMEGA * displacement;
|
|
73
|
+
const values = [];
|
|
74
|
+
let duration = 0;
|
|
75
|
+
while (true) {
|
|
76
|
+
const time = duration / 1000;
|
|
77
|
+
const decay = Math.exp(-SPRING_OMEGA * time);
|
|
78
|
+
const position = (a + b * time) * decay;
|
|
79
|
+
const speed = (b - SPRING_OMEGA * (a + b * time)) * decay;
|
|
80
|
+
const settled = Math.abs(position) < SPRING_REST_DISTANCE && Math.abs(speed) < SPRING_REST_VELOCITY;
|
|
81
|
+
values.push(1 - position / displacement);
|
|
82
|
+
if (duration >= SPRING_MAX_DURATION)
|
|
83
|
+
break;
|
|
84
|
+
if (settled && duration >= SPRING_MIN_DURATION)
|
|
85
|
+
break;
|
|
86
|
+
duration += step;
|
|
87
|
+
}
|
|
88
|
+
values[values.length - 1] = 1;
|
|
89
|
+
return {
|
|
90
|
+
duration,
|
|
91
|
+
easing: `linear(${values.map((value) => value.toFixed(4)).join(", ")})`,
|
|
92
|
+
};
|
|
93
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reshaped",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0-canary.0",
|
|
4
4
|
"description": "Professionally crafted design system in React and Figma for building products of any scale and complexity",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"cssnano": "8.0.2",
|
|
81
81
|
"csstype": "3.2.3",
|
|
82
82
|
"postcss-custom-media": "12.0.1",
|
|
83
|
-
"@reshaped/theming": "4.
|
|
84
|
-
"@reshaped/utilities": "4.
|
|
83
|
+
"@reshaped/theming": "4.3.0-canary.0",
|
|
84
|
+
"@reshaped/utilities": "4.3.0-canary.0"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"postcss": "^8",
|