@seed-design/lynx-react 0.6.2 → 0.7.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/components/Accordion/Accordion.d.ts.map +1 -1
- package/lib/components/Accordion/Accordion.jsx +24 -14
- package/lib/components/ActionButton/ActionButton.d.ts +1 -1
- package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
- package/lib/components/ActionButton/ActionButton.jsx +12 -4
- package/lib/components/AppBar/AppBar.d.ts.map +1 -1
- package/lib/components/AppBar/AppBar.jsx +9 -8
- package/lib/components/Badge/Badge.d.ts.map +1 -1
- package/lib/components/Badge/Badge.jsx +2 -1
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.jsx +9 -9
- package/lib/components/Box/Box.d.ts.map +1 -1
- package/lib/components/Box/Box.jsx +2 -1
- package/lib/components/Callout/Callout.d.ts +2 -2
- package/lib/components/Callout/Callout.d.ts.map +1 -1
- package/lib/components/Callout/Callout.jsx +20 -10
- package/lib/components/Checkbox/Checkbox.d.ts +4 -2
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/lib/components/Checkbox/Checkbox.jsx +33 -17
- package/lib/components/Chip/Chip.d.ts +1 -1
- package/lib/components/Chip/Chip.d.ts.map +1 -1
- package/lib/components/Chip/Chip.jsx +36 -16
- package/lib/components/Count/Count.d.ts +7 -0
- package/lib/components/Count/Count.d.ts.map +1 -0
- package/lib/components/Count/Count.jsx +19 -0
- package/lib/components/Count/index.d.ts +2 -0
- package/lib/components/Count/index.d.ts.map +1 -0
- package/lib/components/Divider/Divider.d.ts.map +1 -1
- package/lib/components/Divider/Divider.jsx +2 -1
- package/lib/components/Field/Field.d.ts.map +1 -1
- package/lib/components/Field/Field.jsx +11 -15
- package/lib/components/Icon/Icon.d.ts.map +1 -1
- package/lib/components/Icon/Icon.jsx +4 -3
- package/lib/components/InputButton/InputButton.d.ts +40 -0
- package/lib/components/InputButton/InputButton.d.ts.map +1 -0
- package/lib/components/InputButton/InputButton.jsx +118 -0
- package/lib/components/InputButton/InputButton.namespace.d.ts +2 -0
- package/lib/components/InputButton/InputButton.namespace.d.ts.map +1 -0
- package/lib/components/InputButton/InputButton.namespace.js +16 -0
- package/lib/components/InputButton/index.d.ts +3 -0
- package/lib/components/InputButton/index.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts.map +1 -1
- package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.jsx +12 -8
- package/lib/components/List/List.d.ts +51 -0
- package/lib/components/List/List.d.ts.map +1 -0
- package/lib/components/List/List.jsx +176 -0
- package/lib/components/List/List.namespace.d.ts +2 -0
- package/lib/components/List/List.namespace.d.ts.map +1 -0
- package/lib/components/List/List.namespace.js +18 -0
- package/lib/components/List/index.d.ts +3 -0
- package/lib/components/List/index.d.ts.map +1 -0
- package/lib/components/MannerTemp/MannerTemp.d.ts.map +1 -1
- package/lib/components/MannerTemp/MannerTemp.jsx +3 -2
- package/lib/components/MannerTempBadge/MannerTempBadge.d.ts.map +1 -1
- package/lib/components/MannerTempBadge/MannerTempBadge.jsx +2 -1
- package/lib/components/Menu/Menu.d.ts +69 -0
- package/lib/components/Menu/Menu.d.ts.map +1 -0
- package/lib/components/Menu/Menu.jsx +520 -0
- package/lib/components/Menu/Menu.namespace.d.ts +2 -0
- package/lib/components/Menu/Menu.namespace.d.ts.map +1 -0
- package/lib/components/Menu/Menu.namespace.js +21 -0
- package/lib/components/Menu/index.d.ts +3 -0
- package/lib/components/Menu/index.d.ts.map +1 -0
- package/lib/components/NotificationBadge/NotificationBadge.d.ts.map +1 -1
- package/lib/components/NotificationBadge/NotificationBadge.jsx +3 -2
- package/lib/components/PageBanner/PageBanner.d.ts +5 -5
- package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
- package/lib/components/PageBanner/PageBanner.jsx +25 -14
- package/lib/components/ProgressCircle/ProgressCircle.d.ts +2 -2
- package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
- package/lib/components/ProgressCircle/ProgressCircle.jsx +4 -3
- package/lib/components/RadioGroup/RadioGroup.d.ts +5 -3
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/lib/components/RadioGroup/RadioGroup.jsx +22 -9
- package/lib/components/ReactionButton/ReactionButton.d.ts +20 -0
- package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -0
- package/lib/components/ReactionButton/ReactionButton.jsx +88 -0
- package/lib/components/ReactionButton/index.d.ts +2 -0
- package/lib/components/ReactionButton/index.d.ts.map +1 -0
- package/lib/components/ScaleFeedback/ScaleFeedback.d.ts +11 -0
- package/lib/components/ScaleFeedback/ScaleFeedback.d.ts.map +1 -0
- package/lib/components/ScaleFeedback/ScaleFeedback.jsx +16 -0
- package/lib/components/ScaleFeedback/index.d.ts +2 -0
- package/lib/components/ScaleFeedback/index.d.ts.map +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.d.ts +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/lib/components/SegmentedControl/SegmentedControl.jsx +23 -10
- package/lib/components/SelectBox/SelectBox.d.ts.map +1 -1
- package/lib/components/SelectBox/SelectBox.jsx +23 -13
- package/lib/components/Stack/Stack.d.ts.map +1 -1
- package/lib/components/Stack/Stack.jsx +2 -1
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts +85 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +256 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.d.ts +2 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.d.ts.map +1 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.js +24 -0
- package/lib/components/SwipeableMenuSheet/index.d.ts +3 -0
- package/lib/components/SwipeableMenuSheet/index.d.ts.map +1 -0
- package/lib/components/Switch/Switch.d.ts +27 -3
- package/lib/components/Switch/Switch.d.ts.map +1 -1
- package/lib/components/Switch/Switch.jsx +22 -9
- package/lib/components/Tabs/Tabs.d.ts +4 -2
- package/lib/components/Tabs/Tabs.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.jsx +37 -22
- package/lib/components/TagGroup/TagGroup.d.ts.map +1 -1
- package/lib/components/TagGroup/TagGroup.jsx +4 -3
- package/lib/components/Text/Text.d.ts.map +1 -1
- package/lib/components/Text/Text.jsx +2 -1
- package/lib/components/TextField/TextField.d.ts.map +1 -1
- package/lib/components/TextField/TextField.jsx +35 -23
- package/lib/components/index.d.ts +7 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/components/private/Positioning.d.ts +69 -0
- package/lib/components/private/Positioning.d.ts.map +1 -0
- package/lib/components/private/Positioning.js +109 -0
- package/lib/components/private/TrackPositioning.d.ts +22 -0
- package/lib/components/private/TrackPositioning.d.ts.map +1 -0
- package/lib/contexts/ScaleFeedbackContentContext.d.ts +4 -0
- package/lib/contexts/ScaleFeedbackContentContext.d.ts.map +1 -0
- package/lib/contexts/ScaleFeedbackContentContext.js +6 -0
- package/lib/contexts/index.d.ts +2 -0
- package/lib/contexts/index.d.ts.map +1 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.d.ts.map +1 -1
- package/lib/hooks/useIconColor.d.ts.map +1 -1
- package/lib/hooks/useIconColor.js +8 -3
- package/lib/hooks/usePressTap.d.ts +4 -3
- package/lib/hooks/usePressTap.d.ts.map +1 -1
- package/lib/hooks/useScaleFeedback.d.ts +48 -0
- package/lib/hooks/useScaleFeedback.d.ts.map +1 -0
- package/lib/hooks/useScaleFeedback.js +108 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +17 -3
- package/lib/node_modules/.bun/@floating-ui_core@1.8.0/node_modules/@floating-ui/core/dist/floating-ui.core.js +493 -0
- package/lib/node_modules/.bun/@floating-ui_utils@0.2.12/node_modules/@floating-ui/utils/dist/floating-ui.utils.js +114 -0
- package/lib/utils/calculate-scale-feedback.d.ts +5 -0
- package/lib/utils/calculate-scale-feedback.d.ts.map +1 -0
- package/lib/utils/calculate-scale-feedback.js +13 -0
- package/lib/utils/compose-event-handlers.d.ts +5 -0
- package/lib/utils/compose-event-handlers.d.ts.map +1 -0
- package/lib/utils/compose-event-handlers.js +16 -0
- package/lib/utils/create-slot-recipe-context.d.ts.map +1 -1
- package/lib/utils/create-slot-recipe-context.jsx +6 -16
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.d.ts.map +1 -1
- package/lib/utils/merge-props.d.ts +15 -0
- package/lib/utils/merge-props.d.ts.map +1 -0
- package/lib/utils/merge-props.js +36 -0
- package/lib/utils/merge-refs.d.ts +8 -0
- package/lib/utils/merge-refs.d.ts.map +1 -0
- package/lib/utils/merge-refs.js +38 -0
- package/package.json +6 -5
- package/src/components/Accordion/Accordion.tsx +36 -27
- package/src/components/ActionButton/ActionButton.tsx +27 -12
- package/src/components/AppBar/AppBar.tsx +17 -18
- package/src/components/Badge/Badge.tsx +2 -2
- package/src/components/BottomSheet/BottomSheet.tsx +9 -17
- package/src/components/Box/Box.tsx +2 -2
- package/src/components/Callout/Callout.tsx +39 -22
- package/src/components/Checkbox/Checkbox.test.tsx +44 -0
- package/src/components/Checkbox/Checkbox.tsx +52 -21
- package/src/components/Chip/Chip.test.tsx +50 -1
- package/src/components/Chip/Chip.tsx +75 -31
- package/src/components/Count/Count.tsx +35 -0
- package/src/components/Count/index.ts +1 -0
- package/src/components/Divider/Divider.tsx +2 -2
- package/src/components/Field/Field.tsx +15 -28
- package/src/components/Icon/Icon.test.tsx +4 -6
- package/src/components/Icon/Icon.tsx +14 -7
- package/src/components/InputButton/InputButton.namespace.ts +20 -0
- package/src/components/InputButton/InputButton.test.tsx +82 -0
- package/src/components/InputButton/InputButton.tsx +295 -0
- package/src/components/InputButton/index.ts +2 -0
- package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.tsx +16 -21
- package/src/components/List/List.namespace.ts +24 -0
- package/src/components/List/List.test.tsx +162 -0
- package/src/components/List/List.tsx +476 -0
- package/src/components/List/index.ts +3 -0
- package/src/components/MannerTemp/MannerTemp.tsx +3 -4
- package/src/components/MannerTempBadge/MannerTempBadge.tsx +2 -2
- package/src/components/Menu/Menu.namespace.ts +26 -0
- package/src/components/Menu/Menu.tsx +883 -0
- package/src/components/Menu/index.ts +31 -0
- package/src/components/NotificationBadge/NotificationBadge.tsx +4 -9
- package/src/components/PageBanner/PageBanner.test.tsx +3 -0
- package/src/components/PageBanner/PageBanner.tsx +54 -45
- package/src/components/ProgressCircle/ProgressCircle.test.tsx +48 -0
- package/src/components/ProgressCircle/ProgressCircle.tsx +22 -3
- package/src/components/RadioGroup/RadioGroup.test.tsx +87 -0
- package/src/components/RadioGroup/RadioGroup.tsx +50 -14
- package/src/components/ReactionButton/ReactionButton.test.tsx +122 -0
- package/src/components/ReactionButton/ReactionButton.tsx +139 -0
- package/src/components/ReactionButton/index.ts +1 -0
- package/src/components/ScaleFeedback/ScaleFeedback.test.tsx +41 -0
- package/src/components/ScaleFeedback/ScaleFeedback.tsx +32 -0
- package/src/components/ScaleFeedback/index.ts +1 -0
- package/src/components/SegmentedControl/SegmentedControl.test.tsx +15 -3
- package/src/components/SegmentedControl/SegmentedControl.tsx +35 -14
- package/src/components/SelectBox/SelectBox.test.tsx +39 -1
- package/src/components/SelectBox/SelectBox.tsx +27 -16
- package/src/components/Stack/Stack.tsx +2 -2
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.ts +37 -0
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +258 -0
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +655 -0
- package/src/components/SwipeableMenuSheet/index.ts +43 -0
- package/src/components/Switch/Switch.test.tsx +57 -0
- package/src/components/Switch/Switch.tsx +60 -16
- package/src/components/Tabs/Tabs.test.tsx +19 -0
- package/src/components/Tabs/Tabs.tsx +72 -40
- package/src/components/TagGroup/TagGroup.tsx +4 -6
- package/src/components/Text/Text.tsx +2 -2
- package/src/components/TextField/TextField.tsx +60 -54
- package/src/components/index.ts +7 -0
- package/src/components/private/Positioning.test.ts +235 -0
- package/src/components/private/Positioning.ts +223 -0
- package/src/components/private/TrackPositioning.test.ts +58 -0
- package/src/components/private/TrackPositioning.ts +37 -0
- package/src/contexts/ScaleFeedbackContentContext.ts +4 -0
- package/src/contexts/index.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useIconColor.test.ts +4 -4
- package/src/hooks/useIconColor.ts +13 -6
- package/src/hooks/usePressTap.ts +4 -3
- package/src/hooks/useScaleFeedback.integration.test.tsx +85 -0
- package/src/hooks/useScaleFeedback.test.ts +50 -0
- package/src/hooks/useScaleFeedback.ts +199 -0
- package/src/index.ts +1 -0
- package/src/utils/calculate-scale-feedback.ts +12 -0
- package/src/utils/compose-event-handlers.ts +16 -0
- package/src/utils/create-slot-recipe-context.tsx +6 -7
- package/src/utils/index.ts +1 -0
- package/src/utils/merge-props.test.tsx +152 -0
- package/src/utils/merge-props.ts +62 -0
- package/src/utils/merge-refs.ts +44 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runOnMainThread,
|
|
3
|
+
runOnBackground,
|
|
4
|
+
useEffect,
|
|
5
|
+
useGlobalProps,
|
|
6
|
+
useMainThreadRef,
|
|
7
|
+
useRef,
|
|
8
|
+
} from "@lynx-js/react";
|
|
9
|
+
import type { RefObject } from "@lynx-js/react";
|
|
10
|
+
import type { MainThread } from "@lynx-js/types";
|
|
11
|
+
import {
|
|
12
|
+
feedbackScaleDuration,
|
|
13
|
+
feedbackScaleTimingFunction,
|
|
14
|
+
} from "@seed-design/lynx-css/scale-feedback";
|
|
15
|
+
|
|
16
|
+
import type { LynxViewProps } from "../types";
|
|
17
|
+
import { calculateScaleFeedback, isReducedMotion } from "../utils/calculate-scale-feedback";
|
|
18
|
+
|
|
19
|
+
type MainThreadLayoutChangeHandler = NonNullable<LynxViewProps["main-thread:bindlayoutchange"]>;
|
|
20
|
+
type MainThreadTouchHandler = NonNullable<LynxViewProps["main-thread:bindtouchstart"]>;
|
|
21
|
+
|
|
22
|
+
interface SeedMotionGlobalProps {
|
|
23
|
+
motion?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ScaleFeedbackElement extends MainThread.Element {
|
|
27
|
+
getComputedStyleProperty(styleName: string): string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseScaleFeedbackOptions {
|
|
31
|
+
/** Disables Scale Feedback while the target is not interactive. */
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/** Runs on the Background Thread immediately after Main Thread touchstart. */
|
|
34
|
+
onTouchStart?: () => void;
|
|
35
|
+
/** Runs on the Background Thread immediately after Main Thread touchend. */
|
|
36
|
+
onTouchEnd?: () => void;
|
|
37
|
+
/** Runs on the Background Thread immediately after Main Thread touchcancel. */
|
|
38
|
+
onTouchCancel?: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ScaleFeedbackTriggerProps {
|
|
42
|
+
"main-thread:bindtouchstart": MainThreadTouchHandler;
|
|
43
|
+
"main-thread:bindtouchend": MainThreadTouchHandler;
|
|
44
|
+
"main-thread:bindtouchcancel": MainThreadTouchHandler;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The target's transform is owned by Scale Feedback; use a separate layer for other transforms. */
|
|
48
|
+
export interface ScaleFeedbackTargetProps {
|
|
49
|
+
flatten: false;
|
|
50
|
+
"main-thread:ref": NonNullable<LynxViewProps["main-thread:ref"]>;
|
|
51
|
+
"main-thread:bindlayoutchange": MainThreadLayoutChangeHandler;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface UseScaleFeedbackReturn {
|
|
55
|
+
/** Spread onto the element that receives touch events. */
|
|
56
|
+
scaleFeedbackTriggerProps: ScaleFeedbackTriggerProps;
|
|
57
|
+
/** Spread onto the element that is measured and scaled. */
|
|
58
|
+
scaleFeedbackTargetProps: ScaleFeedbackTargetProps;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runScaleFeedback(
|
|
62
|
+
targetRef: RefObject<ScaleFeedbackElement | null>,
|
|
63
|
+
animationRef: RefObject<MainThread.Animation | null>,
|
|
64
|
+
scale: number,
|
|
65
|
+
duration: number,
|
|
66
|
+
) {
|
|
67
|
+
"main thread";
|
|
68
|
+
|
|
69
|
+
const target = targetRef.current;
|
|
70
|
+
if (!target) return;
|
|
71
|
+
|
|
72
|
+
let currentTransform = "scale(1)";
|
|
73
|
+
try {
|
|
74
|
+
currentTransform = target.getComputedStyleProperty("transform") || currentTransform;
|
|
75
|
+
} catch {
|
|
76
|
+
// Some non-native runtimes expose the method but cannot evaluate it.
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
animationRef.current?.cancel();
|
|
80
|
+
animationRef.current = target.animate(
|
|
81
|
+
[{ transform: currentTransform }, { transform: `scale(${scale})` }],
|
|
82
|
+
{
|
|
83
|
+
duration,
|
|
84
|
+
easing: feedbackScaleTimingFunction,
|
|
85
|
+
fill: "forwards",
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readScaleFeedbackScale(target: ScaleFeedbackElement | null): number | null {
|
|
91
|
+
"main thread";
|
|
92
|
+
|
|
93
|
+
if (!target) return null;
|
|
94
|
+
|
|
95
|
+
let width = 0;
|
|
96
|
+
let height = 0;
|
|
97
|
+
try {
|
|
98
|
+
// Retry at touchstart when no valid layout measurement is cached,
|
|
99
|
+
// including targets that initially reported a 0x0 layout.
|
|
100
|
+
width = Number.parseFloat(target.getComputedStyleProperty("width"));
|
|
101
|
+
height = Number.parseFloat(target.getComputedStyleProperty("height"));
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (width <= 0 || height <= 0 || Number.isNaN(width) || Number.isNaN(height)) return null;
|
|
107
|
+
|
|
108
|
+
return calculateScaleFeedback(width, height);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Applies SEED Scale Feedback on the Lynx Main Thread.
|
|
113
|
+
*
|
|
114
|
+
* Spread `scaleFeedbackTriggerProps` onto the element that receives touch
|
|
115
|
+
* events and `scaleFeedbackTargetProps` onto the element that should scale.
|
|
116
|
+
* Applying both objects to one element creates Self Scale; applying them to
|
|
117
|
+
* separate elements creates Content Scale.
|
|
118
|
+
*
|
|
119
|
+
* The target's transform is owned by this hook. Use a separate content layer
|
|
120
|
+
* when the element already uses translate, rotate, or another transform.
|
|
121
|
+
*
|
|
122
|
+
* Supported on Lynx Engine 3.9 and later. Missing or unknown
|
|
123
|
+
* `GlobalProps.motion` values preserve the default motion; only the exact
|
|
124
|
+
* `"reduced"` value disables scaling.
|
|
125
|
+
*/
|
|
126
|
+
export function useScaleFeedback(options: UseScaleFeedbackOptions = {}): UseScaleFeedbackReturn {
|
|
127
|
+
const { disabled = false, onTouchStart, onTouchEnd, onTouchCancel } = options;
|
|
128
|
+
const globalProps = useGlobalProps() as SeedMotionGlobalProps | undefined;
|
|
129
|
+
const reducedMotion = isReducedMotion(globalProps?.motion);
|
|
130
|
+
const targetRef = useMainThreadRef<ScaleFeedbackElement | null>(null);
|
|
131
|
+
const animationRef = useMainThreadRef<MainThread.Animation>(null);
|
|
132
|
+
const scaleRef = useMainThreadRef<number | null>(null);
|
|
133
|
+
const hasMountedRef = useRef(false);
|
|
134
|
+
|
|
135
|
+
function handleLayoutChange(event: MainThread.LayoutChangeEvent) {
|
|
136
|
+
"main thread";
|
|
137
|
+
|
|
138
|
+
targetRef.current = event.currentTarget as ScaleFeedbackElement;
|
|
139
|
+
const width = event.detail?.width ?? event.params?.width ?? 0;
|
|
140
|
+
const height = event.detail?.height ?? event.params?.height ?? 0;
|
|
141
|
+
if (width > 0 && height > 0) {
|
|
142
|
+
scaleRef.current = calculateScaleFeedback(width, height);
|
|
143
|
+
} else {
|
|
144
|
+
scaleRef.current = null;
|
|
145
|
+
}
|
|
146
|
+
targetRef.current?.setStyleProperty("transform-origin", "center center");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function handleTouchStart() {
|
|
150
|
+
"main thread";
|
|
151
|
+
|
|
152
|
+
if (!disabled && !reducedMotion) {
|
|
153
|
+
const scale = scaleRef.current ?? readScaleFeedbackScale(targetRef.current);
|
|
154
|
+
if (scale !== null) {
|
|
155
|
+
scaleRef.current = scale;
|
|
156
|
+
runScaleFeedback(targetRef, animationRef, scale, feedbackScaleDuration);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (onTouchStart) runOnBackground(onTouchStart)();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleTouchEnd() {
|
|
163
|
+
"main thread";
|
|
164
|
+
|
|
165
|
+
runScaleFeedback(targetRef, animationRef, 1, feedbackScaleDuration);
|
|
166
|
+
if (onTouchEnd) runOnBackground(onTouchEnd)();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function handleTouchCancel() {
|
|
170
|
+
"main thread";
|
|
171
|
+
|
|
172
|
+
runScaleFeedback(targetRef, animationRef, 1, feedbackScaleDuration);
|
|
173
|
+
if (onTouchCancel) runOnBackground(onTouchCancel)();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!hasMountedRef.current) {
|
|
178
|
+
hasMountedRef.current = true;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!disabled && !reducedMotion) return;
|
|
182
|
+
|
|
183
|
+
runOnMainThread(runScaleFeedback)(targetRef, animationRef, 1, 0);
|
|
184
|
+
}, [disabled, reducedMotion]);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
scaleFeedbackTriggerProps: {
|
|
188
|
+
"main-thread:bindtouchstart": handleTouchStart,
|
|
189
|
+
"main-thread:bindtouchend": handleTouchEnd,
|
|
190
|
+
"main-thread:bindtouchcancel": handleTouchCancel,
|
|
191
|
+
},
|
|
192
|
+
scaleFeedbackTargetProps: {
|
|
193
|
+
// Preserve a native view for transform animations instead of allowing Lynx to flatten it.
|
|
194
|
+
flatten: false,
|
|
195
|
+
"main-thread:ref": targetRef,
|
|
196
|
+
"main-thread:bindlayoutchange": handleLayoutChange,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Calculates SEED's fixed-distance scale ratio directly on the Main Thread. */
|
|
2
|
+
export function calculateScaleFeedback(width: number, height: number): number {
|
|
3
|
+
"main thread";
|
|
4
|
+
|
|
5
|
+
const basis = Math.max(height, width / 4, 24);
|
|
6
|
+
return basis > 0 ? (basis - 2) / basis : 1;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Treats unknown values as default motion and matches only the exact reduced value. */
|
|
10
|
+
export function isReducedMotion(value: unknown): boolean {
|
|
11
|
+
return value === "reduced";
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type Handler = (...args: any[]) => unknown;
|
|
2
|
+
|
|
3
|
+
export function composeEventHandlers(first: Handler, second: Handler): Handler {
|
|
4
|
+
return (...args) => {
|
|
5
|
+
first(...args);
|
|
6
|
+
second(...args);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function composeMainThreadEventHandlers(first: Handler, second: Handler): Handler {
|
|
11
|
+
return (...args) => {
|
|
12
|
+
"main thread";
|
|
13
|
+
first(...args);
|
|
14
|
+
second(...args);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
RefAttributes,
|
|
9
9
|
} from "@lynx-js/react";
|
|
10
10
|
import clsx from "clsx";
|
|
11
|
+
import { mergeProps } from "./merge-props";
|
|
11
12
|
|
|
12
13
|
type SlotRecipe<
|
|
13
14
|
Props extends Record<string, string | boolean | undefined>,
|
|
@@ -105,7 +106,7 @@ export function createSlotRecipeContext<
|
|
|
105
106
|
const { defaultProps } = options ?? {};
|
|
106
107
|
|
|
107
108
|
const StyledComponent = (innerProps: any) => {
|
|
108
|
-
const props =
|
|
109
|
+
const props = mergeProps(defaultProps ?? {}, useProps() ?? {}, innerProps) as Props &
|
|
109
110
|
Record<string, unknown>;
|
|
110
111
|
|
|
111
112
|
restoreDefaultProps(props, defaultProps);
|
|
@@ -139,7 +140,7 @@ export function createSlotRecipeContext<
|
|
|
139
140
|
const { defaultProps } = options ?? {};
|
|
140
141
|
|
|
141
142
|
const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
|
|
142
|
-
const props: any =
|
|
143
|
+
const props: any = mergeProps(defaultProps ?? {}, useProps() ?? {}, innerProps);
|
|
143
144
|
|
|
144
145
|
restoreDefaultProps(props, defaultProps);
|
|
145
146
|
|
|
@@ -152,8 +153,7 @@ export function createSlotRecipeContext<
|
|
|
152
153
|
<ClassNamesProvider value={classNames}>
|
|
153
154
|
<PropsProvider value={variantProps}>
|
|
154
155
|
<Component
|
|
155
|
-
{...(ref ? { ref } : {})}
|
|
156
|
-
{...otherProps}
|
|
156
|
+
{...mergeProps(ref ? { ref } : {}, otherProps)}
|
|
157
157
|
className={clsx(slotClassName, userClassName)}
|
|
158
158
|
/>
|
|
159
159
|
</PropsProvider>
|
|
@@ -177,7 +177,7 @@ export function createSlotRecipeContext<
|
|
|
177
177
|
const { defaultProps } = options ?? {};
|
|
178
178
|
|
|
179
179
|
const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
|
|
180
|
-
const props: any =
|
|
180
|
+
const props: any = mergeProps(defaultProps ?? {}, innerProps);
|
|
181
181
|
|
|
182
182
|
restoreDefaultProps(props, defaultProps);
|
|
183
183
|
|
|
@@ -187,8 +187,7 @@ export function createSlotRecipeContext<
|
|
|
187
187
|
|
|
188
188
|
return (
|
|
189
189
|
<Component
|
|
190
|
-
{...(ref ? { ref } : {})}
|
|
191
|
-
{...props}
|
|
190
|
+
{...mergeProps(ref ? { ref } : {}, props)}
|
|
192
191
|
className={clsx(slotClassName, userClassName)}
|
|
193
192
|
/>
|
|
194
193
|
);
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createRef, runOnBackground, useMainThreadRef } from "@lynx-js/react";
|
|
2
|
+
import { fireEvent, render, waitSchedule } from "@lynx-js/react/testing-library";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { mergeProps } from "./merge-props";
|
|
5
|
+
|
|
6
|
+
describe("mergeProps", () => {
|
|
7
|
+
it("preserves props and chains only identical event keys, user first", () => {
|
|
8
|
+
const calls: string[] = [];
|
|
9
|
+
const base = {
|
|
10
|
+
className: "seed",
|
|
11
|
+
style: { width: "10px", color: "red" },
|
|
12
|
+
bindtap: () => calls.push("base"),
|
|
13
|
+
};
|
|
14
|
+
const user = {
|
|
15
|
+
className: "user",
|
|
16
|
+
style: { color: "blue" },
|
|
17
|
+
bindtap: () => {
|
|
18
|
+
calls.push("user");
|
|
19
|
+
return false;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const props = mergeProps(base, user, { className: undefined });
|
|
23
|
+
props.bindtap();
|
|
24
|
+
expect(calls).toEqual(["user", "base"]);
|
|
25
|
+
expect(props.className).toBe("seed user");
|
|
26
|
+
expect(props.style).toEqual({ width: "10px", color: "blue" });
|
|
27
|
+
expect(base.style.color).toBe("red");
|
|
28
|
+
expect(mergeProps({ bindtap: base.bindtap }, { catchtap: user.bindtap })).toEqual({
|
|
29
|
+
bindtap: base.bindtap,
|
|
30
|
+
catchtap: user.bindtap,
|
|
31
|
+
});
|
|
32
|
+
expect(mergeProps({ ref: null })).toEqual({});
|
|
33
|
+
const ref = createRef();
|
|
34
|
+
expect(mergeProps({ ref }, { ref: null }).ref).toBe(ref);
|
|
35
|
+
expect(mergeProps({ "main-thread:ref": ref })["main-thread:ref"]).toBe(ref);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("preserves three refs across replacement and unmount", async () => {
|
|
39
|
+
const ref = createRef<unknown>();
|
|
40
|
+
const cleanup = vi.fn();
|
|
41
|
+
const first = vi.fn(() => cleanup);
|
|
42
|
+
const next = vi.fn();
|
|
43
|
+
const observer = vi.fn();
|
|
44
|
+
function Example({ callback }: { callback: (value: unknown) => void | (() => void) }) {
|
|
45
|
+
return <view {...mergeProps({ ref }, { ref: callback }, { ref: observer })} />;
|
|
46
|
+
}
|
|
47
|
+
const { rerender, unmount } = render(<Example callback={first} />);
|
|
48
|
+
await waitSchedule();
|
|
49
|
+
expect(ref.current).toBeTruthy();
|
|
50
|
+
expect(first).toHaveBeenLastCalledWith(ref.current);
|
|
51
|
+
expect(observer).toHaveBeenLastCalledWith(ref.current);
|
|
52
|
+
rerender(<Example callback={next} />);
|
|
53
|
+
await waitSchedule();
|
|
54
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
55
|
+
expect(observer.mock.calls[1]).toEqual([null]);
|
|
56
|
+
expect(observer).toHaveBeenLastCalledWith(ref.current);
|
|
57
|
+
unmount();
|
|
58
|
+
await waitSchedule();
|
|
59
|
+
expect(ref.current).toBeNull();
|
|
60
|
+
expect(next).toHaveBeenLastCalledWith(null);
|
|
61
|
+
expect(observer).toHaveBeenLastCalledWith(null);
|
|
62
|
+
expect(observer).toHaveBeenCalledTimes(4);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("composes compiled Main Thread handlers and refreshes captures", async () => {
|
|
66
|
+
function Example({ report }: { report: (value: string) => void }) {
|
|
67
|
+
function base() {
|
|
68
|
+
"main thread";
|
|
69
|
+
runOnBackground(report)("base");
|
|
70
|
+
}
|
|
71
|
+
function user() {
|
|
72
|
+
"main thread";
|
|
73
|
+
runOnBackground(report)("user");
|
|
74
|
+
}
|
|
75
|
+
return (
|
|
76
|
+
<view
|
|
77
|
+
id="target"
|
|
78
|
+
{...mergeProps(
|
|
79
|
+
{ "main-thread:bindtouchstart": base },
|
|
80
|
+
{ "main-thread:bindtouchstart": user },
|
|
81
|
+
)}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const report = vi.fn();
|
|
86
|
+
const next = vi.fn();
|
|
87
|
+
const { container, rerender } = render(<Example report={report} />, {
|
|
88
|
+
enableMainThread: true,
|
|
89
|
+
enableBackgroundThread: true,
|
|
90
|
+
});
|
|
91
|
+
await waitSchedule();
|
|
92
|
+
fireEvent.touchstart(container.querySelector("#target")!, {});
|
|
93
|
+
await waitSchedule();
|
|
94
|
+
expect(report.mock.calls).toEqual([["user"], ["base"]]);
|
|
95
|
+
rerender(<Example report={next} />);
|
|
96
|
+
await waitSchedule();
|
|
97
|
+
fireEvent.touchstart(container.querySelector("#target")!, {});
|
|
98
|
+
await waitSchedule();
|
|
99
|
+
expect(next.mock.calls).toEqual([["user"], ["base"]]);
|
|
100
|
+
expect(report).toHaveBeenCalledTimes(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("composes three Main Thread refs and runs cleanup in source order", async () => {
|
|
104
|
+
function Example({ report, show = true }: { report: (value: string) => void; show?: boolean }) {
|
|
105
|
+
const target = useMainThreadRef(null);
|
|
106
|
+
const status = useMainThreadRef("initial");
|
|
107
|
+
function callback() {
|
|
108
|
+
"main thread";
|
|
109
|
+
status.current = target.current ? "attached" : "missing";
|
|
110
|
+
return () => {
|
|
111
|
+
status.current = target.current === null ? "cleaned" : "uncleared";
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function inspect() {
|
|
115
|
+
"main thread";
|
|
116
|
+
runOnBackground(report)(status.current);
|
|
117
|
+
}
|
|
118
|
+
function observer(value: unknown) {
|
|
119
|
+
"main thread";
|
|
120
|
+
status.current += value ? ":observed" : ":detached";
|
|
121
|
+
}
|
|
122
|
+
return (
|
|
123
|
+
<view>
|
|
124
|
+
<view id="inspect" main-thread:bindtap={inspect} />
|
|
125
|
+
{show && (
|
|
126
|
+
<view
|
|
127
|
+
{...mergeProps(
|
|
128
|
+
{ "main-thread:ref": target },
|
|
129
|
+
{ "main-thread:ref": callback },
|
|
130
|
+
{ "main-thread:ref": observer },
|
|
131
|
+
)}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</view>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const report = vi.fn();
|
|
138
|
+
const { container, rerender } = render(<Example report={report} />, {
|
|
139
|
+
enableMainThread: true,
|
|
140
|
+
enableBackgroundThread: true,
|
|
141
|
+
});
|
|
142
|
+
await waitSchedule();
|
|
143
|
+
fireEvent.tap(container.querySelector("#inspect")!);
|
|
144
|
+
await waitSchedule();
|
|
145
|
+
expect(report.mock.calls).toEqual([["attached:observed"]]);
|
|
146
|
+
rerender(<Example report={report} show={false} />);
|
|
147
|
+
await waitSchedule();
|
|
148
|
+
fireEvent.tap(container.querySelector("#inspect")!);
|
|
149
|
+
await waitSchedule();
|
|
150
|
+
expect(report.mock.calls).toEqual([["attached:observed"], ["cleaned:detached"]]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { composeEventHandlers, composeMainThreadEventHandlers } from "./compose-event-handlers";
|
|
3
|
+
import { mergeRefs, mergeMainThreadRefs, type Ref } from "./merge-refs";
|
|
4
|
+
|
|
5
|
+
type Props = Record<string, any>;
|
|
6
|
+
type Merge<A, B> = Omit<A, keyof B> & {
|
|
7
|
+
[K in keyof B]: undefined extends B[K]
|
|
8
|
+
? Exclude<B[K], undefined> | (K extends keyof A ? A[K] : undefined)
|
|
9
|
+
: B[K];
|
|
10
|
+
};
|
|
11
|
+
type Merged<T extends object[], Result = {}> = T extends [
|
|
12
|
+
infer First extends object,
|
|
13
|
+
...infer Rest extends object[],
|
|
14
|
+
]
|
|
15
|
+
? Merged<Rest, Merge<Result, First>>
|
|
16
|
+
: Result;
|
|
17
|
+
|
|
18
|
+
const nativeEvent = /^(?:main-thread:)?(?:(?:capture|global)-)?(?:bind|catch)[a-z]/;
|
|
19
|
+
const callbackEvent = /^on[A-Z]/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Merges props without changing event threads or propagation modes.
|
|
23
|
+
* Same-key handlers run last-source first, matching SEED React. Return values
|
|
24
|
+
* and preventDefault do not cancel another handler. Ref cleanup is preserved.
|
|
25
|
+
*/
|
|
26
|
+
export function mergeProps<T extends object[]>(...sources: T): Merged<T> {
|
|
27
|
+
const result: Props = {};
|
|
28
|
+
const refs: Ref<unknown>[] = [];
|
|
29
|
+
const mainThreadRefs: Ref<unknown>[] = [];
|
|
30
|
+
for (const source of sources) {
|
|
31
|
+
for (const [key, value] of Object.entries(source)) {
|
|
32
|
+
if (value === undefined) continue;
|
|
33
|
+
const previous = result[key];
|
|
34
|
+
if (key === "ref" || key === "main-thread:ref") {
|
|
35
|
+
if (value == null) continue;
|
|
36
|
+
const targetRefs = key === "ref" ? refs : mainThreadRefs;
|
|
37
|
+
targetRefs.push(value as Ref<unknown>);
|
|
38
|
+
} else if (key === "className") {
|
|
39
|
+
result[key] = clsx(previous, value as string);
|
|
40
|
+
} else if (key === "style" && typeof value === "object" && value !== null) {
|
|
41
|
+
result[key] = { ...(typeof previous === "object" ? previous : {}), ...value };
|
|
42
|
+
} else if (nativeEvent.test(key) || callbackEvent.test(key)) {
|
|
43
|
+
if (previous && value) {
|
|
44
|
+
const compose = key.startsWith("main-thread:")
|
|
45
|
+
? composeMainThreadEventHandlers
|
|
46
|
+
: composeEventHandlers;
|
|
47
|
+
result[key] = compose(value as any, previous);
|
|
48
|
+
} else {
|
|
49
|
+
result[key] = value;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
result[key] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (refs.length) result["ref"] = refs.length === 1 ? refs[0] : mergeRefs(...refs);
|
|
57
|
+
if (mainThreadRefs.length) {
|
|
58
|
+
result["main-thread:ref"] =
|
|
59
|
+
mainThreadRefs.length === 1 ? mainThreadRefs[0] : mergeMainThreadRefs(...mainThreadRefs);
|
|
60
|
+
}
|
|
61
|
+
return result as Merged<T>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type Cleanup = void | (() => void);
|
|
2
|
+
export type Ref<T> = { current: T | null } | ((value: T | null) => Cleanup);
|
|
3
|
+
|
|
4
|
+
function setRef<T>(ref: Ref<T>, value: T | null): Cleanup {
|
|
5
|
+
if (typeof ref === "function") return ref(value);
|
|
6
|
+
ref.current = value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function setMainThreadRef<T>(ref: Ref<T>, value: T | null): Cleanup {
|
|
10
|
+
"main thread";
|
|
11
|
+
if (typeof ref === "function") return ref(value);
|
|
12
|
+
ref.current = value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function mergeRefs<T>(...refs: Ref<T>[]): Ref<T> {
|
|
16
|
+
return (value) => {
|
|
17
|
+
const cleanups = refs.map((ref) => setRef(ref, value));
|
|
18
|
+
return () => {
|
|
19
|
+
refs.forEach((ref, index) => {
|
|
20
|
+
const cleanup = cleanups[index];
|
|
21
|
+
if (cleanup) cleanup();
|
|
22
|
+
else setRef(ref, null);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function mergeMainThreadRefs<T>(...refs: Ref<T>[]): Ref<T> {
|
|
29
|
+
return (value) => {
|
|
30
|
+
"main thread";
|
|
31
|
+
const cleanups: Cleanup[] = [];
|
|
32
|
+
// Indexed access preserves the captured array in the Main Thread compiler.
|
|
33
|
+
for (let index = 0; index < refs.length; index++) {
|
|
34
|
+
cleanups.push(setMainThreadRef(refs[index], value));
|
|
35
|
+
}
|
|
36
|
+
return () => {
|
|
37
|
+
for (let index = 0; index < refs.length; index++) {
|
|
38
|
+
const cleanup = cleanups[index];
|
|
39
|
+
if (cleanup) cleanup();
|
|
40
|
+
else setMainThreadRef(refs[index], null);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|