@seed-design/lynx-react 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/lib/components/RadioGroup/RadioGroup.jsx +2 -1
- package/lib/components/Tabs/Tabs.d.ts +1 -1
- package/lib/components/Tabs/Tabs.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.jsx +63 -43
- package/lib/components/Tabs/Tabs.utils.d.ts +19 -0
- package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -0
- package/lib/components/Tabs/Tabs.utils.js +40 -0
- package/lib/components/TextField/TextField.d.ts.map +1 -1
- package/lib/components/TextField/TextField.jsx +4 -0
- package/package.json +2 -2
- package/src/components/RadioGroup/RadioGroup.tsx +2 -1
- package/src/components/Tabs/Tabs.test.tsx +65 -0
- package/src/components/Tabs/Tabs.tsx +82 -49
- package/src/components/Tabs/Tabs.utils.ts +63 -0
- package/src/components/TextField/TextField.test.tsx +48 -0
- package/src/components/TextField/TextField.tsx +8 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RadioGroup.d.ts","sourceRoot":"","sources":["../../../src/components/RadioGroup/RadioGroup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAInE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAE7E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAKrF,OAAO,KAAK,EACV,oBAAoB,EACpB,sBAAsB,EAGvB,MAAM,aAAa,CAAC;AAsErB,MAAM,WAAW,mBACf,SAAQ,iBAAiB,EACvB,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC,EAC5D,sBAAsB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,eAAO,MAAM,cAAc,qFAyDzB,CAAC;AAKH,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,cAAc,qFA+CzB,CAAC;AAKH,MAAM,WAAW,0BACf,SAAQ,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC,EACzC,sBAAsB;CAAG;AAE7B,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"RadioGroup.d.ts","sourceRoot":"","sources":["../../../src/components/RadioGroup/RadioGroup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAInE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAE7E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAKrF,OAAO,KAAK,EACV,oBAAoB,EACpB,sBAAsB,EAGvB,MAAM,aAAa,CAAC;AAsErB,MAAM,WAAW,mBACf,SAAQ,iBAAiB,EACvB,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC,EAC5D,sBAAsB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,eAAO,MAAM,cAAc,qFAyDzB,CAAC;AAKH,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,cAAc,qFA+CzB,CAAC;AAKH,MAAM,WAAW,0BACf,SAAQ,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC,EACzC,sBAAsB;CAAG;AAE7B,eAAO,MAAM,qBAAqB,4FA8BjC,CAAC;AAKF,MAAM,WAAW,4BACf,SAAQ,IAAI,CAAC,sBAAsB,EAAE,WAAW,GAAG,OAAO,CAAC;IAC3D,kGAAkG;IAClG,SAAS,CAAC,EAAE,YAAY,CAAC,oBAAoB,CAAC,CAAC;IAC/C,0GAA0G;IAC1G,OAAO,CAAC,EAAE,YAAY,CAAC,oBAAoB,CAAC,CAAC;CAC9C;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,4BAA4B,qBA6B1E;yBA7Be,uBAAuB;;;AAkCvC,MAAM,WAAW,wBAAyB,SAAQ,sBAAsB;CAAG;AAE3E,eAAO,MAAM,mBAAmB,0FAoB/B,CAAC"}
|
|
@@ -119,11 +119,12 @@ var RadioGroupItemControl = React.forwardRef((props, ref) => {
|
|
|
119
119
|
pressed: itemContext.pressed
|
|
120
120
|
};
|
|
121
121
|
const classes = radiomark(radiomarkVariantProps);
|
|
122
|
+
const controlClassName = radio(groupContext.radioVariantProps).control;
|
|
122
123
|
return <RadiomarkControlContext.Provider value={{
|
|
123
124
|
iconClassName: classes.icon,
|
|
124
125
|
radiomarkVariantProps
|
|
125
126
|
}}>
|
|
126
|
-
<view {...ref ? { ref } : {}} className={clsx(classes.root, className)} {...nativeProps}>
|
|
127
|
+
<view {...ref ? { ref } : {}} className={clsx(classes.root, controlClassName, className)} {...nativeProps}>
|
|
127
128
|
{children}
|
|
128
129
|
</view>
|
|
129
130
|
</RadiomarkControlContext.Provider>;
|
|
@@ -3,7 +3,7 @@ import { IntrinsicElements } from '@lynx-js/types';
|
|
|
3
3
|
import { LynxAccessibilityProps, LynxStyledElementProps } from '../../types';
|
|
4
4
|
import * as React from "@lynx-js/react";
|
|
5
5
|
type NativeViewPagerProps = IntrinsicElements["viewpager"];
|
|
6
|
-
type TabsPublicVariantProps = Omit<TabsVariantProps, "selected" | "disabled" | "inCarousel">;
|
|
6
|
+
type TabsPublicVariantProps = Omit<TabsVariantProps, "selected" | "disabled" | "inCarousel" | "transitionEnabled">;
|
|
7
7
|
/**
|
|
8
8
|
* @platform Lynx
|
|
9
9
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,EACV,iBAAiB,EAMlB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EAGvB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,EACV,iBAAiB,EAMlB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EAGvB,MAAM,aAAa,CAAC;AAWrB,KAAK,oBAAoB,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;AAI3D,KAAK,sBAAsB,GAAG,IAAI,CAChC,gBAAgB,EAChB,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,mBAAmB,CAC7D,CAAC;AAkFF;;;;;;;GAOG;AACH,MAAM,WAAW,aAAc,SAAQ,sBAAsB,EAAE,sBAAsB;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,eAAO,MAAM,QAAQ,+EAoNnB,CAAC;AAyBH,MAAM,WAAW,aAAc,SAAQ,sBAAsB;CAAG;AAEhE,eAAO,MAAM,QAAQ,+EAkCnB,CAAC;AAKH,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB,CAAC,EAAE,sBAAsB,CAAC,qBAAqB,CAAC,CAAC;CACvE;AAED,eAAO,MAAM,WAAW,kFAkEtB,CAAC;AAKH,MAAM,WAAW,kBAAmB,SAAQ,sBAAsB;CAAG;AAErE,eAAO,MAAM,aAAa,oFAiCxB,CAAC;AAKH,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,WAAW,kFAgCtB,CAAC;AAKH;;;;;GAKG;AACH,MAAM,WAAW,iBAAkB,SAAQ,sBAAsB;IAC/D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2CAA2C;IAC3C,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAED,eAAO,MAAM,YAAY,mFA8BvB,CAAC;AAKH,MAAM,WAAW,uBAAwB,SAAQ,sBAAsB;IACrE,UAAU,CAAC,EAAE,oBAAoB,CAAC,YAAY,CAAC,CAAC;IAChD,cAAc,CAAC,EAAE,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IACxD,gBAAgB,CAAC,EAAE,oBAAoB,CAAC,kBAAkB,CAAC,CAAC;CAC7D;AAED,eAAO,MAAM,kBAAkB,yFAgH9B,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useControllableState } from "../../hooks/useControllableState.js";
|
|
2
2
|
import { usePressTap } from "../../hooks/usePressTap.js";
|
|
3
3
|
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context.jsx";
|
|
4
|
+
import { areTabsTransitionsEnabled, getTabsLayoutWidth, getTabsOrderedItems, getTabsTriggerRects } from "./Tabs.utils.js";
|
|
4
5
|
import * as React from "@lynx-js/react";
|
|
5
6
|
import clsx from "clsx";
|
|
6
7
|
import { tabs } from "@seed-design/lynx-css/recipes/tabs";
|
|
@@ -32,15 +33,6 @@ function invokeScrollToOffset(list, offset) {
|
|
|
32
33
|
}).exec();
|
|
33
34
|
} catch {}
|
|
34
35
|
}
|
|
35
|
-
function getLayoutRect(event) {
|
|
36
|
-
const width = event.detail?.width ?? event.params?.width;
|
|
37
|
-
const left = event.detail?.left ?? event.params?.left;
|
|
38
|
-
if (!Number.isFinite(width) || !Number.isFinite(left)) return null;
|
|
39
|
-
return {
|
|
40
|
-
width: Math.max(0, width),
|
|
41
|
-
left
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
36
|
var TabsContext = React.createContext(null);
|
|
45
37
|
function useTabsContext(consumer) {
|
|
46
38
|
const context = React.useContext(TabsContext);
|
|
@@ -60,7 +52,6 @@ function useTabsCarouselContext(consumer) {
|
|
|
60
52
|
var TabsRoot = React.forwardRef((props, ref) => {
|
|
61
53
|
const [variantProps, otherProps] = tabs.splitVariantProps(props);
|
|
62
54
|
const { children, className, style, value: valueProp, defaultValue, onValueChange, ...nativeProps } = otherProps;
|
|
63
|
-
const classNames = tabs(variantProps);
|
|
64
55
|
const [value, setValueInternal] = useControllableState({
|
|
65
56
|
value: valueProp,
|
|
66
57
|
defaultValue,
|
|
@@ -72,17 +63,21 @@ var TabsRoot = React.forwardRef((props, ref) => {
|
|
|
72
63
|
const [items, setItems] = React.useState([]);
|
|
73
64
|
const [contentValues, setContentValues] = React.useState([]);
|
|
74
65
|
const [indicatorValue, setIndicatorValue] = React.useState();
|
|
75
|
-
const [
|
|
76
|
-
const [listLeft, setListLeft] = React.useState(0);
|
|
66
|
+
const [triggerWidths, setTriggerWidths] = React.useState({});
|
|
77
67
|
const listRef = React.useRef(null);
|
|
78
68
|
const pagerRef = React.useRef(null);
|
|
79
69
|
const indicatorRef = React.useMainThreadRef(null);
|
|
80
70
|
const pagerValues = React.useMemo(() => contentValues.filter((contentValue) => !items.find((item) => item.value === contentValue)?.disabled), [contentValues, items]);
|
|
71
|
+
const triggerRects = React.useMemo(() => getTabsTriggerRects(items.map((item) => item.value), triggerWidths), [items, triggerWidths]);
|
|
72
|
+
const transitionsEnabled = areTabsTransitionsEnabled(items.map((item) => item.value), triggerRects);
|
|
73
|
+
const classNames = tabs({
|
|
74
|
+
...variantProps,
|
|
75
|
+
transitionEnabled: transitionsEnabled
|
|
76
|
+
});
|
|
81
77
|
const visualValue = indicatorValue ?? value;
|
|
82
78
|
const indicatorIndex = items.findIndex((item) => item.value === visualValue);
|
|
83
79
|
const selectedPagerIndex = value === void 0 ? -1 : pagerValues.indexOf(value);
|
|
84
|
-
const
|
|
85
|
-
const selectedOffset = selectedRect ? Math.max(0, selectedRect.left - listLeft) : null;
|
|
80
|
+
const selectedOffset = (value === void 0 ? void 0 : triggerRects[value])?.left ?? null;
|
|
86
81
|
const setListNode = React.useCallback((node) => {
|
|
87
82
|
listRef.current = node;
|
|
88
83
|
if (node && selectedOffset !== null) invokeScrollToOffset(node, selectedOffset);
|
|
@@ -102,7 +97,7 @@ var TabsRoot = React.forwardRef((props, ref) => {
|
|
|
102
97
|
return () => {
|
|
103
98
|
"background only";
|
|
104
99
|
setItems((current) => current.filter((item) => item.value !== triggerValue));
|
|
105
|
-
|
|
100
|
+
setTriggerWidths((current) => {
|
|
106
101
|
const next = { ...current };
|
|
107
102
|
delete next[triggerValue];
|
|
108
103
|
return next;
|
|
@@ -122,14 +117,16 @@ var TabsRoot = React.forwardRef((props, ref) => {
|
|
|
122
117
|
disabled
|
|
123
118
|
} : item));
|
|
124
119
|
}, []);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
120
|
+
const syncTriggerOrder = React.useCallback((triggerValues) => {
|
|
121
|
+
setItems((current) => {
|
|
122
|
+
const ordered = getTabsOrderedItems(current, triggerValues);
|
|
123
|
+
return ordered.every((item, index) => item === current[index]) ? current : ordered;
|
|
124
|
+
});
|
|
125
|
+
}, []);
|
|
126
|
+
const updateTriggerWidth = React.useCallback((triggerValue, width) => {
|
|
127
|
+
setTriggerWidths((current) => current[triggerValue] === width ? current : {
|
|
128
|
+
...current,
|
|
129
|
+
[triggerValue]: width
|
|
133
130
|
});
|
|
134
131
|
}, []);
|
|
135
132
|
const selectValue = React.useCallback((nextValue) => {
|
|
@@ -159,13 +156,13 @@ var TabsRoot = React.forwardRef((props, ref) => {
|
|
|
159
156
|
indicatorIndex,
|
|
160
157
|
selectedPagerIndex,
|
|
161
158
|
indicatorRef,
|
|
162
|
-
|
|
159
|
+
transitionsEnabled,
|
|
163
160
|
triggerRects,
|
|
164
161
|
registerTrigger,
|
|
165
162
|
registerContent,
|
|
166
163
|
updateTriggerDisabled,
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
syncTriggerOrder,
|
|
165
|
+
updateTriggerWidth,
|
|
169
166
|
setListRef: setListNode,
|
|
170
167
|
setPagerRef: setPagerNode,
|
|
171
168
|
selectValue,
|
|
@@ -180,12 +177,13 @@ var TabsRoot = React.forwardRef((props, ref) => {
|
|
|
180
177
|
indicatorIndex,
|
|
181
178
|
selectedPagerIndex,
|
|
182
179
|
indicatorRef,
|
|
183
|
-
listLeft,
|
|
184
180
|
triggerRects,
|
|
181
|
+
transitionsEnabled,
|
|
185
182
|
registerTrigger,
|
|
186
183
|
registerContent,
|
|
187
184
|
updateTriggerDisabled,
|
|
188
|
-
|
|
185
|
+
syncTriggerOrder,
|
|
186
|
+
updateTriggerWidth,
|
|
189
187
|
setListNode,
|
|
190
188
|
setPagerNode,
|
|
191
189
|
selectValue,
|
|
@@ -201,21 +199,42 @@ var TabsRoot = React.forwardRef((props, ref) => {
|
|
|
201
199
|
</TabsContext.Provider>;
|
|
202
200
|
});
|
|
203
201
|
TabsRoot.displayName = "TabsRoot";
|
|
202
|
+
function getTabsTriggerValues(children) {
|
|
203
|
+
const values = [];
|
|
204
|
+
function visit(node) {
|
|
205
|
+
if (Array.isArray(node)) {
|
|
206
|
+
node.forEach(visit);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!React.isValidElement(node)) return;
|
|
210
|
+
if (node.type === TabsTrigger) {
|
|
211
|
+
values.push(node.props.value);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (node.type === React.Fragment) visit(node.props.children);
|
|
215
|
+
}
|
|
216
|
+
visit(children);
|
|
217
|
+
return values;
|
|
218
|
+
}
|
|
204
219
|
var TabsList = React.forwardRef((props, ref) => {
|
|
205
220
|
const { children, className, style, ...nativeProps } = props;
|
|
206
221
|
const classNames = useClassNames();
|
|
207
|
-
const {
|
|
222
|
+
const { items, setListRef, syncTriggerOrder } = useTabsContext("TabsList");
|
|
223
|
+
const triggerOrder = React.useMemo(() => getTabsTriggerValues(children), [children]);
|
|
224
|
+
React.useEffect(() => {
|
|
225
|
+
"background only";
|
|
226
|
+
syncTriggerOrder(triggerOrder);
|
|
227
|
+
}, [
|
|
228
|
+
items,
|
|
229
|
+
syncTriggerOrder,
|
|
230
|
+
triggerOrder
|
|
231
|
+
]);
|
|
208
232
|
const mergedRef = React.useCallback((node) => {
|
|
209
233
|
setListRef(node);
|
|
210
234
|
if (typeof ref === "function") ref(node);
|
|
211
235
|
else if (ref) ref.current = node;
|
|
212
236
|
}, [ref, setListRef]);
|
|
213
|
-
|
|
214
|
-
"background only";
|
|
215
|
-
const rect = getLayoutRect(args[0]);
|
|
216
|
-
if (rect) setListLeft(rect.left);
|
|
217
|
-
}, [setListLeft]);
|
|
218
|
-
return <scroll-view ref={mergedRef} {...nativeProps} scroll-orientation="horizontal" scroll-bar-enable={false} bindlayoutchange={handleLayoutChange} accessibility-element={false} accessibility-traits="tabbar" className={clsx(classNames.list, className)} style={style}>
|
|
237
|
+
return <scroll-view ref={mergedRef} {...nativeProps} scroll-orientation="horizontal" scroll-bar-enable={false} accessibility-element={false} accessibility-traits="tabbar" className={clsx(classNames.list, className)} style={style}>
|
|
219
238
|
<view className={classNames.listContent}>{children}</view>
|
|
220
239
|
</scroll-view>;
|
|
221
240
|
});
|
|
@@ -246,13 +265,14 @@ var TabsTrigger = React.forwardRef((props, ref) => {
|
|
|
246
265
|
});
|
|
247
266
|
const handleLayoutChange = React.useCallback((...args) => {
|
|
248
267
|
"background only";
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
251
|
-
}, [context.
|
|
268
|
+
const width = getTabsLayoutWidth(args[0]);
|
|
269
|
+
if (width !== null) context.updateTriggerWidth(triggerValue, width);
|
|
270
|
+
}, [context.updateTriggerWidth, triggerValue]);
|
|
252
271
|
const triggerClasses = tabs({
|
|
253
272
|
...context.variantProps,
|
|
254
273
|
selected: visuallySelected,
|
|
255
|
-
disabled
|
|
274
|
+
disabled,
|
|
275
|
+
transitionEnabled: context.transitionsEnabled
|
|
256
276
|
});
|
|
257
277
|
const label = typeof children === "string" || typeof children === "number" ? String(children) : void 0;
|
|
258
278
|
return <view {...ref ? { ref } : {}} {...nativeProps} {...pressHandlers} flatten={false} bindlayoutchange={handleLayoutChange} accessibility-element={true} accessibility-role-description="tab" accessibility-label={accessibilityLabel ?? label} accessibility-value={selected ? "selected" : "not selected"} accessibility-traits={disabled ? "disabled" : selected ? "selected" : "button"} className={clsx(triggerClasses.trigger, className)} style={style}>
|
|
@@ -263,14 +283,14 @@ TabsTrigger.displayName = "TabsTrigger";
|
|
|
263
283
|
var TabsIndicator = React.forwardRef((props, ref) => {
|
|
264
284
|
const { className, style, ...nativeProps } = props;
|
|
265
285
|
const classNames = useClassNames();
|
|
266
|
-
const { indicatorRef, items,
|
|
286
|
+
const { indicatorRef, items, indicatorIndex, triggerRects } = useTabsContext("TabsIndicator");
|
|
267
287
|
const position = indicatorIndex;
|
|
268
288
|
const lowerIndex = Math.max(0, Math.floor(position));
|
|
269
289
|
const upperIndex = Math.min(items.length - 1, Math.ceil(position));
|
|
270
290
|
const progress = Math.max(0, Math.min(1, position - lowerIndex));
|
|
271
291
|
const lowerRect = triggerRects[items[lowerIndex]?.value ?? ""];
|
|
272
292
|
const upperRect = triggerRects[items[upperIndex]?.value ?? ""] ?? lowerRect;
|
|
273
|
-
const x = lowerRect ? lowerRect.left
|
|
293
|
+
const x = lowerRect ? lowerRect.left + ((upperRect?.left ?? lowerRect.left) - lowerRect.left) * progress : 0;
|
|
274
294
|
const width = lowerRect ? lowerRect.width + ((upperRect?.width ?? lowerRect.width) - lowerRect.width) * progress : 0;
|
|
275
295
|
return <view {...ref ? { ref } : {}} main-thread:ref={indicatorRef} {...nativeProps} accessibility-elements-hidden={true} className={clsx(classNames.indicator, className)} style={{
|
|
276
296
|
"--tabs-indicator-x": `${x}px`,
|
|
@@ -337,7 +357,7 @@ var TabsCarouselCamera = React.forwardRef((props, ref) => {
|
|
|
337
357
|
const tabsContext = useTabsContext("TabsCarouselCamera");
|
|
338
358
|
const carouselContext = useTabsCarouselContext("TabsCarouselCamera");
|
|
339
359
|
const swipingRef = React.useRef(false);
|
|
340
|
-
const { indicatorRef, pagerValues,
|
|
360
|
+
const { indicatorRef, pagerValues, triggerRects } = tabsContext;
|
|
341
361
|
const indicatorRects = pagerValues.map((value) => triggerRects[value] ?? null);
|
|
342
362
|
const mergedRef = React.useCallback((node) => {
|
|
343
363
|
tabsContext.setPagerRef(node);
|
|
@@ -388,7 +408,7 @@ var TabsCarouselCamera = React.forwardRef((props, ref) => {
|
|
|
388
408
|
const lowerRect = indicatorRects[lowerIndex];
|
|
389
409
|
const upperRect = indicatorRects[upperIndex] ?? lowerRect;
|
|
390
410
|
if (!lowerRect) return;
|
|
391
|
-
const x = lowerRect.left
|
|
411
|
+
const x = lowerRect.left + ((upperRect?.left ?? lowerRect.left) - lowerRect.left) * progress;
|
|
392
412
|
const width = lowerRect.width + ((upperRect?.width ?? lowerRect.width) - lowerRect.width) * progress;
|
|
393
413
|
indicatorRef.current?.setStyleProperties({
|
|
394
414
|
"--tabs-indicator-x": `${x}px`,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface TabsLayoutChangeEvent {
|
|
2
|
+
detail?: {
|
|
3
|
+
width?: number;
|
|
4
|
+
};
|
|
5
|
+
params?: {
|
|
6
|
+
width?: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface TabsLayoutRect {
|
|
10
|
+
left: number;
|
|
11
|
+
width: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function getTabsLayoutWidth(event: TabsLayoutChangeEvent): number | null;
|
|
14
|
+
export declare function getTabsOrderedItems<T extends {
|
|
15
|
+
value: string;
|
|
16
|
+
}>(items: T[], values: string[]): T[];
|
|
17
|
+
export declare function getTabsTriggerRects(values: string[], widths: Record<string, number>): Record<string, TabsLayoutRect>;
|
|
18
|
+
export declare function areTabsTransitionsEnabled(values: string[], rects: Record<string, TabsLayoutRect>): boolean;
|
|
19
|
+
//# sourceMappingURL=Tabs.utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tabs.utils.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/Tabs.utils.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,CAI9E;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAC7D,KAAK,EAAE,CAAC,EAAE,EACV,MAAM,EAAE,MAAM,EAAE,GACf,CAAC,EAAE,CAUL;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAiBhC;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,EAAE,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GACpC,OAAO,CAET"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/components/Tabs/Tabs.utils.ts
|
|
2
|
+
function getTabsLayoutWidth(event) {
|
|
3
|
+
const width = event.detail?.width ?? event.params?.width;
|
|
4
|
+
if (typeof width !== "number" || !Number.isFinite(width)) return null;
|
|
5
|
+
return Math.max(0, width);
|
|
6
|
+
}
|
|
7
|
+
function getTabsOrderedItems(items, values) {
|
|
8
|
+
const itemsByValue = new Map(items.map((item) => [item.value, item]));
|
|
9
|
+
const ordered = values.flatMap((value) => {
|
|
10
|
+
const item = itemsByValue.get(value);
|
|
11
|
+
if (!item) return [];
|
|
12
|
+
itemsByValue.delete(value);
|
|
13
|
+
return [item];
|
|
14
|
+
});
|
|
15
|
+
ordered.push(...itemsByValue.values());
|
|
16
|
+
return ordered;
|
|
17
|
+
}
|
|
18
|
+
function getTabsTriggerRects(values, widths) {
|
|
19
|
+
const rects = Object.create(null);
|
|
20
|
+
let left = 0;
|
|
21
|
+
let hasCompletePrefix = true;
|
|
22
|
+
for (const value of values) {
|
|
23
|
+
const width = Object.prototype.hasOwnProperty.call(widths, value) ? widths[value] : void 0;
|
|
24
|
+
if (width === void 0) {
|
|
25
|
+
hasCompletePrefix = false;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (hasCompletePrefix) rects[value] = {
|
|
29
|
+
left,
|
|
30
|
+
width
|
|
31
|
+
};
|
|
32
|
+
left += width;
|
|
33
|
+
}
|
|
34
|
+
return rects;
|
|
35
|
+
}
|
|
36
|
+
function areTabsTransitionsEnabled(values, rects) {
|
|
37
|
+
return values.length > 0 && values.every((value) => rects[value] !== void 0);
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { areTabsTransitionsEnabled, getTabsLayoutWidth, getTabsOrderedItems, getTabsTriggerRects };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../src/components/TextField/TextField.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAa,KAAK,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAIjG,OAAO,KAAK,EAAE,sBAAsB,EAAE,sBAAsB,EAAe,MAAM,aAAa,CAAC;AAG/F,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AA+BpE;;;;;;GAMG;AACH,MAAM,WAAW,kBACf,SAAQ,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,EAC5C,sBAAsB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,aAAa,qFAuHzB,CAAC;AAKF,KAAK,gBAAgB,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACnD,KAAK,mBAAmB,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../src/components/TextField/TextField.tsx"],"names":[],"mappings":"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"}
|
|
@@ -257,12 +257,16 @@ function useNativeTextControl({ forwardedRef, disabled: disabledProp, readOnly:
|
|
|
257
257
|
]);
|
|
258
258
|
const handleBlur = React.useCallback((event) => {
|
|
259
259
|
"background only";
|
|
260
|
+
const nativeValue = event.detail?.value;
|
|
261
|
+
if (typeof nativeValue === "string") lastNativeValueRef.current = nativeValue;
|
|
260
262
|
textFieldContext.setFocused(false);
|
|
261
263
|
keyboardAvoidance?.blur(ownerRef.current);
|
|
262
264
|
bindblur?.(event);
|
|
265
|
+
if (typeof nativeValue === "string") reconcileControlledValue(nativeValue);
|
|
263
266
|
}, [
|
|
264
267
|
bindblur,
|
|
265
268
|
keyboardAvoidance,
|
|
269
|
+
reconcileControlledValue,
|
|
266
270
|
textFieldContext.setFocused
|
|
267
271
|
]);
|
|
268
272
|
const notifyLayoutChanged = React.useCallback(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-design/lynx-react",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/daangn/seed-design.git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@lynx-js/react": "^0.117.0",
|
|
41
41
|
"@lynx-js/types": "^3.7.0",
|
|
42
|
-
"@seed-design/lynx-css": "0.9.
|
|
42
|
+
"@seed-design/lynx-css": "0.9.3",
|
|
43
43
|
"@testing-library/jest-dom": "^6.9.1",
|
|
44
44
|
"jsdom": "^29.0.2",
|
|
45
45
|
"preact": "npm:@lynx-js/internal-preact@10.28.4-ee7bb26",
|
|
@@ -232,6 +232,7 @@ export const RadioGroupItemControl = React.forwardRef<unknown, RadioGroupItemCon
|
|
|
232
232
|
pressed: itemContext.pressed,
|
|
233
233
|
};
|
|
234
234
|
const classes = radiomark(radiomarkVariantProps);
|
|
235
|
+
const controlClassName = radio(groupContext.radioVariantProps).control;
|
|
235
236
|
|
|
236
237
|
return (
|
|
237
238
|
<RadiomarkControlContext.Provider
|
|
@@ -239,7 +240,7 @@ export const RadioGroupItemControl = React.forwardRef<unknown, RadioGroupItemCon
|
|
|
239
240
|
>
|
|
240
241
|
<view
|
|
241
242
|
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
242
|
-
className={clsx(classes.root, className)}
|
|
243
|
+
className={clsx(classes.root, controlClassName, className)}
|
|
243
244
|
{...nativeProps}
|
|
244
245
|
>
|
|
245
246
|
{children}
|
|
@@ -3,6 +3,12 @@ import { fireEvent, render } from "@lynx-js/react/testing-library";
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
import * as Tabs from "./Tabs.namespace";
|
|
6
|
+
import {
|
|
7
|
+
areTabsTransitionsEnabled,
|
|
8
|
+
getTabsLayoutWidth,
|
|
9
|
+
getTabsOrderedItems,
|
|
10
|
+
getTabsTriggerRects,
|
|
11
|
+
} from "./Tabs.utils";
|
|
6
12
|
|
|
7
13
|
function BasicTabs(props: {
|
|
8
14
|
value?: string;
|
|
@@ -40,6 +46,26 @@ describe("Tabs", () => {
|
|
|
40
46
|
expect(listContent?.querySelectorAll(".seed-tabs__trigger")).toHaveLength(2);
|
|
41
47
|
});
|
|
42
48
|
|
|
49
|
+
it("disables label and indicator transitions during the initial render", () => {
|
|
50
|
+
const { container } = render(<BasicTabs defaultValue="two" />);
|
|
51
|
+
const labels = container.querySelectorAll<HTMLElement>(".seed-tabs__triggerLabel");
|
|
52
|
+
const indicator = container.querySelector<HTMLElement>(".seed-tabs__indicator");
|
|
53
|
+
|
|
54
|
+
expect(indicator).toHaveClass("seed-tabs__indicator--transitionEnabled_false");
|
|
55
|
+
expect(indicator?.style.transitionDuration).toBe("");
|
|
56
|
+
for (const label of labels) {
|
|
57
|
+
expect(label).toHaveClass("seed-tabs__triggerLabel--transitionEnabled_false");
|
|
58
|
+
expect(label.style.transitionDuration).toBe("");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("disables transitions when a dynamically added trigger is unmeasured", () => {
|
|
63
|
+
const rects = getTabsTriggerRects(["one", "two"], { one: 80, two: 80 });
|
|
64
|
+
|
|
65
|
+
expect(areTabsTransitionsEnabled(["one", "two"], rects)).toBe(true);
|
|
66
|
+
expect(areTabsTransitionsEnabled(["one", "two", "three"], rects)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
43
69
|
it("changes an uncontrolled value when a trigger is tapped", () => {
|
|
44
70
|
const onValueChange = vi.fn();
|
|
45
71
|
const { container } = render(<BasicTabs defaultValue="one" onValueChange={onValueChange} />);
|
|
@@ -56,6 +82,45 @@ describe("Tabs", () => {
|
|
|
56
82
|
expect(second).toHaveAttribute("accessibility-value", "selected");
|
|
57
83
|
});
|
|
58
84
|
|
|
85
|
+
it("derives trigger positions from ordered widths instead of platform coordinates", () => {
|
|
86
|
+
expect(getTabsLayoutWidth({ detail: { width: 128 }, params: { width: 128 } })).toBe(128);
|
|
87
|
+
expect(
|
|
88
|
+
getTabsTriggerRects(["one", "two", "three"], {
|
|
89
|
+
one: 128,
|
|
90
|
+
two: 128,
|
|
91
|
+
three: 128,
|
|
92
|
+
}),
|
|
93
|
+
).toEqual({
|
|
94
|
+
one: { left: 0, width: 128 },
|
|
95
|
+
two: { left: 128, width: 128 },
|
|
96
|
+
three: { left: 256, width: 128 },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("waits for every preceding trigger width before positioning later triggers", () => {
|
|
101
|
+
expect(getTabsTriggerRects(["one", "two", "three"], { one: 80, three: 120 })).toEqual({
|
|
102
|
+
one: { left: 0, width: 80 },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("ignores inherited object properties used as trigger values", () => {
|
|
107
|
+
expect(getTabsTriggerRects(["toString"], {})).toEqual({});
|
|
108
|
+
expect(getTabsTriggerRects(["constructor"], {})).toEqual({});
|
|
109
|
+
expect(getTabsTriggerRects(["__proto__"], {})).toEqual({});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("updates trigger positions when keyed triggers are reordered", () => {
|
|
113
|
+
const items = [{ value: "one" }, { value: "two" }, { value: "three" }];
|
|
114
|
+
const reordered = getTabsOrderedItems(items, ["three", "two", "one"]);
|
|
115
|
+
const rects = getTabsTriggerRects(
|
|
116
|
+
reordered.map((item) => item.value),
|
|
117
|
+
{ one: 80, two: 80, three: 80 },
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(reordered.map((item) => item.value)).toEqual(["three", "two", "one"]);
|
|
121
|
+
expect(rects["three"]?.left).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
59
124
|
it("does not select a disabled trigger", () => {
|
|
60
125
|
const onValueChange = vi.fn();
|
|
61
126
|
const { container } = render(
|
|
@@ -19,13 +19,23 @@ import type {
|
|
|
19
19
|
LynxViewRef,
|
|
20
20
|
} from "../../types";
|
|
21
21
|
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
22
|
+
import {
|
|
23
|
+
areTabsTransitionsEnabled,
|
|
24
|
+
getTabsLayoutWidth,
|
|
25
|
+
getTabsOrderedItems,
|
|
26
|
+
getTabsTriggerRects,
|
|
27
|
+
type TabsLayoutRect,
|
|
28
|
+
} from "./Tabs.utils";
|
|
22
29
|
|
|
23
30
|
type NativeViewProps = IntrinsicElements["view"];
|
|
24
31
|
type NativeViewPagerProps = IntrinsicElements["viewpager"];
|
|
25
32
|
type LayoutChangeHandler = NonNullable<NativeViewProps["bindlayoutchange"]>;
|
|
26
|
-
type TriggerRect =
|
|
33
|
+
type TriggerRect = TabsLayoutRect;
|
|
27
34
|
type TriggerItem = { value: string; disabled: boolean };
|
|
28
|
-
type TabsPublicVariantProps = Omit<
|
|
35
|
+
type TabsPublicVariantProps = Omit<
|
|
36
|
+
TabsVariantProps,
|
|
37
|
+
"selected" | "disabled" | "inCarousel" | "transitionEnabled"
|
|
38
|
+
>;
|
|
29
39
|
|
|
30
40
|
const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(tabs);
|
|
31
41
|
|
|
@@ -49,13 +59,6 @@ function invokeScrollToOffset(list: NodesRef | null, offset: number) {
|
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
function getLayoutRect(event: Parameters<LayoutChangeHandler>[0]): TriggerRect | null {
|
|
53
|
-
const width = event.detail?.width ?? event.params?.width;
|
|
54
|
-
const left = event.detail?.left ?? event.params?.left;
|
|
55
|
-
if (!Number.isFinite(width) || !Number.isFinite(left)) return null;
|
|
56
|
-
return { width: Math.max(0, width), left };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
62
|
interface TabsContextValue {
|
|
60
63
|
value: string | undefined;
|
|
61
64
|
visualValue: string | undefined;
|
|
@@ -65,13 +68,13 @@ interface TabsContextValue {
|
|
|
65
68
|
indicatorIndex: number;
|
|
66
69
|
selectedPagerIndex: number;
|
|
67
70
|
indicatorRef: React.RefObject<MainThread.Element>;
|
|
68
|
-
listLeft: number;
|
|
69
71
|
triggerRects: Record<string, TriggerRect>;
|
|
72
|
+
transitionsEnabled: boolean;
|
|
70
73
|
registerTrigger: (value: string, disabled: boolean) => () => void;
|
|
71
74
|
registerContent: (value: string) => () => void;
|
|
72
75
|
updateTriggerDisabled: (value: string, disabled: boolean) => void;
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
syncTriggerOrder: (values: string[]) => void;
|
|
77
|
+
updateTriggerWidth: (value: string, width: number) => void;
|
|
75
78
|
setListRef: (ref: NodesRef | null) => void;
|
|
76
79
|
setPagerRef: (ref: NodesRef | null) => void;
|
|
77
80
|
selectValue: (value: string) => void;
|
|
@@ -139,7 +142,6 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
139
142
|
onValueChange,
|
|
140
143
|
...nativeProps
|
|
141
144
|
} = otherProps;
|
|
142
|
-
const classNames = tabs(variantProps);
|
|
143
145
|
const [value, setValueInternal] = useControllableState<string | undefined>({
|
|
144
146
|
value: valueProp,
|
|
145
147
|
defaultValue,
|
|
@@ -151,8 +153,7 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
151
153
|
const [items, setItems] = React.useState<TriggerItem[]>([]);
|
|
152
154
|
const [contentValues, setContentValues] = React.useState<string[]>([]);
|
|
153
155
|
const [indicatorValue, setIndicatorValue] = React.useState<string | undefined>();
|
|
154
|
-
const [
|
|
155
|
-
const [listLeft, setListLeft] = React.useState(0);
|
|
156
|
+
const [triggerWidths, setTriggerWidths] = React.useState<Record<string, number>>({});
|
|
156
157
|
const listRef = React.useRef<NodesRef | null>(null);
|
|
157
158
|
const pagerRef = React.useRef<NodesRef | null>(null);
|
|
158
159
|
const indicatorRef = React.useMainThreadRef<MainThread.Element>(null);
|
|
@@ -164,11 +165,24 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
164
165
|
),
|
|
165
166
|
[contentValues, items],
|
|
166
167
|
);
|
|
168
|
+
const triggerRects = React.useMemo(
|
|
169
|
+
() =>
|
|
170
|
+
getTabsTriggerRects(
|
|
171
|
+
items.map((item) => item.value),
|
|
172
|
+
triggerWidths,
|
|
173
|
+
),
|
|
174
|
+
[items, triggerWidths],
|
|
175
|
+
);
|
|
176
|
+
const transitionsEnabled = areTabsTransitionsEnabled(
|
|
177
|
+
items.map((item) => item.value),
|
|
178
|
+
triggerRects,
|
|
179
|
+
);
|
|
180
|
+
const classNames = tabs({ ...variantProps, transitionEnabled: transitionsEnabled });
|
|
167
181
|
const visualValue = indicatorValue ?? value;
|
|
168
182
|
const indicatorIndex = items.findIndex((item) => item.value === visualValue);
|
|
169
183
|
const selectedPagerIndex = value === undefined ? -1 : pagerValues.indexOf(value);
|
|
170
184
|
const selectedRect = value === undefined ? undefined : triggerRects[value];
|
|
171
|
-
const selectedOffset = selectedRect
|
|
185
|
+
const selectedOffset = selectedRect?.left ?? null;
|
|
172
186
|
|
|
173
187
|
const setListNode = React.useCallback(
|
|
174
188
|
(node: NodesRef | null) => {
|
|
@@ -195,7 +209,7 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
195
209
|
return () => {
|
|
196
210
|
"background only";
|
|
197
211
|
setItems((current) => current.filter((item) => item.value !== triggerValue));
|
|
198
|
-
|
|
212
|
+
setTriggerWidths((current) => {
|
|
199
213
|
const next = { ...current };
|
|
200
214
|
delete next[triggerValue];
|
|
201
215
|
return next;
|
|
@@ -222,14 +236,19 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
222
236
|
);
|
|
223
237
|
}, []);
|
|
224
238
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
return { ...current, [triggerValue]: rect };
|
|
239
|
+
const syncTriggerOrder = React.useCallback((triggerValues: string[]) => {
|
|
240
|
+
setItems((current) => {
|
|
241
|
+
const ordered = getTabsOrderedItems(current, triggerValues);
|
|
242
|
+
return ordered.every((item, index) => item === current[index]) ? current : ordered;
|
|
230
243
|
});
|
|
231
244
|
}, []);
|
|
232
245
|
|
|
246
|
+
const updateTriggerWidth = React.useCallback((triggerValue: string, width: number) => {
|
|
247
|
+
setTriggerWidths((current) =>
|
|
248
|
+
current[triggerValue] === width ? current : { ...current, [triggerValue]: width },
|
|
249
|
+
);
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
233
252
|
const selectValue = React.useCallback(
|
|
234
253
|
(nextValue: string) => {
|
|
235
254
|
setIndicatorValue(undefined);
|
|
@@ -273,13 +292,13 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
273
292
|
indicatorIndex,
|
|
274
293
|
selectedPagerIndex,
|
|
275
294
|
indicatorRef,
|
|
276
|
-
|
|
295
|
+
transitionsEnabled,
|
|
277
296
|
triggerRects,
|
|
278
297
|
registerTrigger,
|
|
279
298
|
registerContent,
|
|
280
299
|
updateTriggerDisabled,
|
|
281
|
-
|
|
282
|
-
|
|
300
|
+
syncTriggerOrder,
|
|
301
|
+
updateTriggerWidth,
|
|
283
302
|
setListRef: setListNode,
|
|
284
303
|
setPagerRef: setPagerNode,
|
|
285
304
|
selectValue,
|
|
@@ -295,12 +314,13 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
295
314
|
indicatorIndex,
|
|
296
315
|
selectedPagerIndex,
|
|
297
316
|
indicatorRef,
|
|
298
|
-
listLeft,
|
|
299
317
|
triggerRects,
|
|
318
|
+
transitionsEnabled,
|
|
300
319
|
registerTrigger,
|
|
301
320
|
registerContent,
|
|
302
321
|
updateTriggerDisabled,
|
|
303
|
-
|
|
322
|
+
syncTriggerOrder,
|
|
323
|
+
updateTriggerWidth,
|
|
304
324
|
setListNode,
|
|
305
325
|
setPagerNode,
|
|
306
326
|
selectValue,
|
|
@@ -328,12 +348,38 @@ TabsRoot.displayName = "TabsRoot";
|
|
|
328
348
|
|
|
329
349
|
////////////////////////////////////////////////////////////////////////////////////
|
|
330
350
|
|
|
351
|
+
function getTabsTriggerValues(children: React.ReactNode): string[] {
|
|
352
|
+
const values: string[] = [];
|
|
353
|
+
|
|
354
|
+
function visit(node: React.ReactNode) {
|
|
355
|
+
if (Array.isArray(node)) {
|
|
356
|
+
node.forEach(visit);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!React.isValidElement<TabsTriggerProps>(node)) return;
|
|
360
|
+
if (node.type === TabsTrigger) {
|
|
361
|
+
values.push(node.props.value);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (node.type === React.Fragment) visit(node.props.children);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
visit(children);
|
|
368
|
+
return values;
|
|
369
|
+
}
|
|
370
|
+
|
|
331
371
|
export interface TabsListProps extends LynxStyledElementProps {}
|
|
332
372
|
|
|
333
373
|
export const TabsList = React.forwardRef<unknown, TabsListProps>((props, ref) => {
|
|
334
374
|
const { children, className, style, ...nativeProps } = props;
|
|
335
375
|
const classNames = useClassNames();
|
|
336
|
-
const {
|
|
376
|
+
const { items, setListRef, syncTriggerOrder } = useTabsContext("TabsList");
|
|
377
|
+
const triggerOrder = React.useMemo(() => getTabsTriggerValues(children), [children]);
|
|
378
|
+
|
|
379
|
+
React.useEffect(() => {
|
|
380
|
+
"background only";
|
|
381
|
+
syncTriggerOrder(triggerOrder);
|
|
382
|
+
}, [items, syncTriggerOrder, triggerOrder]);
|
|
337
383
|
|
|
338
384
|
const mergedRef = React.useCallback(
|
|
339
385
|
(node: NodesRef | null) => {
|
|
@@ -344,22 +390,12 @@ export const TabsList = React.forwardRef<unknown, TabsListProps>((props, ref) =>
|
|
|
344
390
|
[ref, setListRef],
|
|
345
391
|
);
|
|
346
392
|
|
|
347
|
-
const handleLayoutChange = React.useCallback<LayoutChangeHandler>(
|
|
348
|
-
(...args) => {
|
|
349
|
-
"background only";
|
|
350
|
-
const rect = getLayoutRect(args[0]);
|
|
351
|
-
if (rect) setListLeft(rect.left);
|
|
352
|
-
},
|
|
353
|
-
[setListLeft],
|
|
354
|
-
);
|
|
355
|
-
|
|
356
393
|
return (
|
|
357
394
|
<scroll-view
|
|
358
395
|
ref={mergedRef}
|
|
359
396
|
{...nativeProps}
|
|
360
397
|
scroll-orientation="horizontal"
|
|
361
398
|
scroll-bar-enable={false}
|
|
362
|
-
bindlayoutchange={handleLayoutChange}
|
|
363
399
|
accessibility-element={false}
|
|
364
400
|
accessibility-traits="tabbar"
|
|
365
401
|
className={clsx(classNames.list, className)}
|
|
@@ -412,16 +448,17 @@ export const TabsTrigger = React.forwardRef<unknown, TabsTriggerProps>((props, r
|
|
|
412
448
|
const handleLayoutChange = React.useCallback<LayoutChangeHandler>(
|
|
413
449
|
(...args) => {
|
|
414
450
|
"background only";
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
451
|
+
const width = getTabsLayoutWidth(args[0]);
|
|
452
|
+
if (width !== null) context.updateTriggerWidth(triggerValue, width);
|
|
417
453
|
},
|
|
418
|
-
[context.
|
|
454
|
+
[context.updateTriggerWidth, triggerValue],
|
|
419
455
|
);
|
|
420
456
|
|
|
421
457
|
const triggerClasses = tabs({
|
|
422
458
|
...context.variantProps,
|
|
423
459
|
selected: visuallySelected,
|
|
424
460
|
disabled,
|
|
461
|
+
transitionEnabled: context.transitionsEnabled,
|
|
425
462
|
});
|
|
426
463
|
const label =
|
|
427
464
|
typeof children === "string" || typeof children === "number" ? String(children) : undefined;
|
|
@@ -454,8 +491,7 @@ export interface TabsIndicatorProps extends LynxStyledElementProps {}
|
|
|
454
491
|
export const TabsIndicator = React.forwardRef<unknown, TabsIndicatorProps>((props, ref) => {
|
|
455
492
|
const { className, style, ...nativeProps } = props;
|
|
456
493
|
const classNames = useClassNames();
|
|
457
|
-
const { indicatorRef, items,
|
|
458
|
-
useTabsContext("TabsIndicator");
|
|
494
|
+
const { indicatorRef, items, indicatorIndex, triggerRects } = useTabsContext("TabsIndicator");
|
|
459
495
|
const position = indicatorIndex;
|
|
460
496
|
const lowerIndex = Math.max(0, Math.floor(position));
|
|
461
497
|
const upperIndex = Math.min(items.length - 1, Math.ceil(position));
|
|
@@ -463,7 +499,7 @@ export const TabsIndicator = React.forwardRef<unknown, TabsIndicatorProps>((prop
|
|
|
463
499
|
const lowerRect = triggerRects[items[lowerIndex]?.value ?? ""];
|
|
464
500
|
const upperRect = triggerRects[items[upperIndex]?.value ?? ""] ?? lowerRect;
|
|
465
501
|
const x = lowerRect
|
|
466
|
-
? lowerRect.left
|
|
502
|
+
? lowerRect.left + ((upperRect?.left ?? lowerRect.left) - lowerRect.left) * progress
|
|
467
503
|
: 0;
|
|
468
504
|
const width = lowerRect
|
|
469
505
|
? lowerRect.width + ((upperRect?.width ?? lowerRect.width) - lowerRect.width) * progress
|
|
@@ -602,7 +638,7 @@ export const TabsCarouselCamera = React.forwardRef<unknown, TabsCarouselCameraPr
|
|
|
602
638
|
const tabsContext = useTabsContext("TabsCarouselCamera");
|
|
603
639
|
const carouselContext = useTabsCarouselContext("TabsCarouselCamera");
|
|
604
640
|
const swipingRef = React.useRef(false);
|
|
605
|
-
const { indicatorRef, pagerValues,
|
|
641
|
+
const { indicatorRef, pagerValues, triggerRects } = tabsContext;
|
|
606
642
|
const indicatorRects = pagerValues.map((value) => triggerRects[value] ?? null);
|
|
607
643
|
|
|
608
644
|
const mergedRef = React.useCallback(
|
|
@@ -669,10 +705,7 @@ export const TabsCarouselCamera = React.forwardRef<unknown, TabsCarouselCameraPr
|
|
|
669
705
|
const upperRect = indicatorRects[upperIndex] ?? lowerRect;
|
|
670
706
|
if (!lowerRect) return;
|
|
671
707
|
|
|
672
|
-
const x =
|
|
673
|
-
lowerRect.left -
|
|
674
|
-
listLeft +
|
|
675
|
-
((upperRect?.left ?? lowerRect.left) - lowerRect.left) * progress;
|
|
708
|
+
const x = lowerRect.left + ((upperRect?.left ?? lowerRect.left) - lowerRect.left) * progress;
|
|
676
709
|
const width =
|
|
677
710
|
lowerRect.width + ((upperRect?.width ?? lowerRect.width) - lowerRect.width) * progress;
|
|
678
711
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface TabsLayoutChangeEvent {
|
|
2
|
+
detail?: {
|
|
3
|
+
width?: number;
|
|
4
|
+
};
|
|
5
|
+
params?: {
|
|
6
|
+
width?: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TabsLayoutRect {
|
|
11
|
+
left: number;
|
|
12
|
+
width: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getTabsLayoutWidth(event: TabsLayoutChangeEvent): number | null {
|
|
16
|
+
const width = event.detail?.width ?? event.params?.width;
|
|
17
|
+
if (typeof width !== "number" || !Number.isFinite(width)) return null;
|
|
18
|
+
return Math.max(0, width);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getTabsOrderedItems<T extends { value: string }>(
|
|
22
|
+
items: T[],
|
|
23
|
+
values: string[],
|
|
24
|
+
): T[] {
|
|
25
|
+
const itemsByValue = new Map(items.map((item) => [item.value, item]));
|
|
26
|
+
const ordered = values.flatMap((value) => {
|
|
27
|
+
const item = itemsByValue.get(value);
|
|
28
|
+
if (!item) return [];
|
|
29
|
+
itemsByValue.delete(value);
|
|
30
|
+
return [item];
|
|
31
|
+
});
|
|
32
|
+
ordered.push(...itemsByValue.values());
|
|
33
|
+
return ordered;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getTabsTriggerRects(
|
|
37
|
+
values: string[],
|
|
38
|
+
widths: Record<string, number>,
|
|
39
|
+
): Record<string, TabsLayoutRect> {
|
|
40
|
+
const rects = Object.create(null) as Record<string, TabsLayoutRect>;
|
|
41
|
+
let left = 0;
|
|
42
|
+
let hasCompletePrefix = true;
|
|
43
|
+
|
|
44
|
+
for (const value of values) {
|
|
45
|
+
// biome-ignore lint/suspicious/noPrototypeBuiltins: Object.hasOwn is unavailable in supported Lynx runtimes.
|
|
46
|
+
const width = Object.prototype.hasOwnProperty.call(widths, value) ? widths[value] : undefined;
|
|
47
|
+
if (width === undefined) {
|
|
48
|
+
hasCompletePrefix = false;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (hasCompletePrefix) rects[value] = { left, width };
|
|
52
|
+
left += width;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return rects;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function areTabsTransitionsEnabled(
|
|
59
|
+
values: string[],
|
|
60
|
+
rects: Record<string, TabsLayoutRect>,
|
|
61
|
+
): boolean {
|
|
62
|
+
return values.length > 0 && values.every((value) => rects[value] !== undefined);
|
|
63
|
+
}
|
|
@@ -301,6 +301,54 @@ describe("TextField", () => {
|
|
|
301
301
|
expect(invoke).not.toHaveBeenCalled();
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
it("restores the committed controlled value when blur reports a stale native value", async () => {
|
|
305
|
+
const inputRef = createRef<NodesRef>();
|
|
306
|
+
|
|
307
|
+
function ControlledInput() {
|
|
308
|
+
const [value, setValue] = useState("초기값");
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<TextField.Root value={value} onValueChange={setValue}>
|
|
312
|
+
<TextField.Input ref={inputRef} />
|
|
313
|
+
</TextField.Root>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
render(<ControlledInput />);
|
|
318
|
+
|
|
319
|
+
const input = getRenderedRoot().querySelector("input");
|
|
320
|
+
if (!input || !inputRef.current) throw new Error("Expected native input to exist.");
|
|
321
|
+
|
|
322
|
+
const exec = vi.fn();
|
|
323
|
+
const invoke = vi.fn(() => ({ exec }));
|
|
324
|
+
inputRef.current.invoke = invoke as unknown as NodesRef["invoke"];
|
|
325
|
+
|
|
326
|
+
fireNativeEvent(inputRef.current, input, "input", {
|
|
327
|
+
value: "최신값",
|
|
328
|
+
selectionStart: 3,
|
|
329
|
+
selectionEnd: 3,
|
|
330
|
+
isComposing: false,
|
|
331
|
+
});
|
|
332
|
+
await flushControlledReconciliation();
|
|
333
|
+
|
|
334
|
+
fireNativeEvent(inputRef.current, input, "blur", { value: "이전값" });
|
|
335
|
+
await flushControlledReconciliation();
|
|
336
|
+
|
|
337
|
+
expect(invoke).toHaveBeenCalledTimes(1);
|
|
338
|
+
expect(invoke).toHaveBeenCalledWith(
|
|
339
|
+
expect.objectContaining({
|
|
340
|
+
method: "setValue",
|
|
341
|
+
params: { value: "최신값" },
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
345
|
+
|
|
346
|
+
fireNativeEvent(inputRef.current, input, "blur", { value: "최신값" });
|
|
347
|
+
await flushControlledReconciliation();
|
|
348
|
+
|
|
349
|
+
expect(invoke).toHaveBeenCalledTimes(1);
|
|
350
|
+
});
|
|
351
|
+
|
|
304
352
|
it("waits for a parent value update queued in a microtask", async () => {
|
|
305
353
|
const inputRef = createRef<NodesRef>();
|
|
306
354
|
|
|
@@ -456,11 +456,18 @@ function useNativeTextControl({
|
|
|
456
456
|
(event: Parameters<NonNullable<NativeInputProps["bindblur"]>>[0]) => {
|
|
457
457
|
"background only";
|
|
458
458
|
|
|
459
|
+
const nativeValue = event.detail?.value;
|
|
460
|
+
if (typeof nativeValue === "string") {
|
|
461
|
+
lastNativeValueRef.current = nativeValue;
|
|
462
|
+
}
|
|
459
463
|
textFieldContext.setFocused(false);
|
|
460
464
|
keyboardAvoidance?.blur(ownerRef.current);
|
|
461
465
|
bindblur?.(event);
|
|
466
|
+
if (typeof nativeValue === "string") {
|
|
467
|
+
reconcileControlledValue(nativeValue);
|
|
468
|
+
}
|
|
462
469
|
},
|
|
463
|
-
[bindblur, keyboardAvoidance, textFieldContext.setFocused],
|
|
470
|
+
[bindblur, keyboardAvoidance, reconcileControlledValue, textFieldContext.setFocused],
|
|
464
471
|
);
|
|
465
472
|
|
|
466
473
|
const notifyLayoutChanged = React.useCallback(() => {
|