@seed-design/lynx-react 0.0.0-alpha-20260622094810
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/_virtual/_rolldown/runtime.js +13 -0
- package/lib/components/ActionButton/ActionButton.d.ts +49 -0
- package/lib/components/ActionButton/ActionButton.d.ts.map +1 -0
- package/lib/components/ActionButton/ActionButton.jsx +158 -0
- package/lib/components/ActionButton/index.d.ts +2 -0
- package/lib/components/ActionButton/index.d.ts.map +1 -0
- package/lib/components/AppBar/AppBar.d.ts +35 -0
- package/lib/components/AppBar/AppBar.d.ts.map +1 -0
- package/lib/components/AppBar/AppBar.jsx +132 -0
- package/lib/components/AppBar/AppBar.namespace.d.ts +2 -0
- package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -0
- package/lib/components/AppBar/AppBar.namespace.js +15 -0
- package/lib/components/AppBar/context.d.ts +13 -0
- package/lib/components/AppBar/context.d.ts.map +1 -0
- package/lib/components/AppBar/context.js +12 -0
- package/lib/components/AppBar/index.d.ts +3 -0
- package/lib/components/AppBar/index.d.ts.map +1 -0
- package/lib/components/AppBar/useAppBar.d.ts +15 -0
- package/lib/components/AppBar/useAppBar.d.ts.map +1 -0
- package/lib/components/AppBar/useAppBar.js +72 -0
- package/lib/components/Badge/Badge.d.ts +14 -0
- package/lib/components/Badge/Badge.d.ts.map +1 -0
- package/lib/components/Badge/Badge.jsx +15 -0
- package/lib/components/Badge/index.d.ts +2 -0
- package/lib/components/Badge/index.d.ts.map +1 -0
- package/lib/components/BottomSheet/BottomSheet.d.ts +87 -0
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/lib/components/BottomSheet/BottomSheet.jsx +163 -0
- package/lib/components/BottomSheet/BottomSheet.namespace.d.ts +2 -0
- package/lib/components/BottomSheet/BottomSheet.namespace.d.ts.map +1 -0
- package/lib/components/BottomSheet/BottomSheet.namespace.js +18 -0
- package/lib/components/BottomSheet/index.d.ts +3 -0
- package/lib/components/BottomSheet/index.d.ts.map +1 -0
- package/lib/components/Box/Box.d.ts +20 -0
- package/lib/components/Box/Box.d.ts.map +1 -0
- package/lib/components/Box/Box.jsx +14 -0
- package/lib/components/Box/index.d.ts +2 -0
- package/lib/components/Box/index.d.ts.map +1 -0
- package/lib/components/Checkbox/Checkbox.d.ts +35 -0
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/lib/components/Checkbox/Checkbox.jsx +130 -0
- package/lib/components/Checkbox/Checkbox.namespace.d.ts +2 -0
- package/lib/components/Checkbox/Checkbox.namespace.d.ts.map +1 -0
- package/lib/components/Checkbox/Checkbox.namespace.js +12 -0
- package/lib/components/Checkbox/index.d.ts +3 -0
- package/lib/components/Checkbox/index.d.ts.map +1 -0
- package/lib/components/Icon/Icon.d.ts +37 -0
- package/lib/components/Icon/Icon.d.ts.map +1 -0
- package/lib/components/Icon/Icon.jsx +143 -0
- package/lib/components/Icon/index.d.ts +2 -0
- package/lib/components/Icon/index.d.ts.map +1 -0
- package/lib/components/ProgressCircle/ProgressCircle.d.ts +25 -0
- package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -0
- package/lib/components/ProgressCircle/ProgressCircle.jsx +281 -0
- package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts +2 -0
- package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts.map +1 -0
- package/lib/components/ProgressCircle/ProgressCircle.namespace.js +9 -0
- package/lib/components/ProgressCircle/index.d.ts +3 -0
- package/lib/components/ProgressCircle/index.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.d.ts +34 -0
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.jsx +160 -0
- package/lib/components/RadioGroup/RadioGroup.namespace.d.ts +2 -0
- package/lib/components/RadioGroup/RadioGroup.namespace.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.namespace.js +12 -0
- package/lib/components/RadioGroup/index.d.ts +3 -0
- package/lib/components/RadioGroup/index.d.ts.map +1 -0
- package/lib/components/Stack/Stack.d.ts +33 -0
- package/lib/components/Stack/Stack.d.ts.map +1 -0
- package/lib/components/Stack/Stack.jsx +39 -0
- package/lib/components/Stack/index.d.ts +2 -0
- package/lib/components/Stack/index.d.ts.map +1 -0
- package/lib/components/Switch/Switch.d.ts +21 -0
- package/lib/components/Switch/Switch.d.ts.map +1 -0
- package/lib/components/Switch/Switch.jsx +101 -0
- package/lib/components/Switch/Switch.namespace.d.ts +2 -0
- package/lib/components/Switch/Switch.namespace.d.ts.map +1 -0
- package/lib/components/Switch/Switch.namespace.js +11 -0
- package/lib/components/Switch/index.d.ts +3 -0
- package/lib/components/Switch/index.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.d.ts +34 -0
- package/lib/components/TagGroup/TagGroup.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.jsx +57 -0
- package/lib/components/TagGroup/TagGroup.namespace.d.ts +2 -0
- package/lib/components/TagGroup/TagGroup.namespace.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.namespace.js +10 -0
- package/lib/components/TagGroup/index.d.ts +3 -0
- package/lib/components/TagGroup/index.d.ts.map +1 -0
- package/lib/components/Text/Text.d.ts +7 -0
- package/lib/components/Text/Text.d.ts.map +1 -0
- package/lib/components/Text/Text.jsx +28 -0
- package/lib/components/Text/index.d.ts +2 -0
- package/lib/components/Text/index.d.ts.map +1 -0
- package/lib/components/index.d.ts +14 -0
- package/lib/components/index.d.ts.map +1 -0
- package/lib/hooks/index.d.ts +5 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/useControllableState.d.ts +15 -0
- package/lib/hooks/useControllableState.d.ts.map +1 -0
- package/lib/hooks/useControllableState.js +27 -0
- package/lib/hooks/useIconColor.d.ts +19 -0
- package/lib/hooks/useIconColor.d.ts.map +1 -0
- package/lib/hooks/useIconColor.js +38 -0
- package/lib/hooks/usePressTap.d.ts +27 -0
- package/lib/hooks/usePressTap.d.ts.map +1 -0
- package/lib/hooks/usePressTap.js +35 -0
- package/lib/hooks/useSafeArea.d.ts +19 -0
- package/lib/hooks/useSafeArea.d.ts.map +1 -0
- package/lib/hooks/useSafeArea.js +33 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +29 -0
- package/lib/types.d.ts +51 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/utils/children.d.ts +3 -0
- package/lib/utils/children.d.ts.map +1 -0
- package/lib/utils/children.js +8 -0
- package/lib/utils/create-slot-recipe-context.d.ts +27 -0
- package/lib/utils/create-slot-recipe-context.d.ts.map +1 -0
- package/lib/utils/create-slot-recipe-context.jsx +94 -0
- package/lib/utils/get-seed-class-name.d.ts +22 -0
- package/lib/utils/get-seed-class-name.d.ts.map +1 -0
- package/lib/utils/get-seed-class-name.js +28 -0
- package/lib/utils/index.d.ts +5 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/resolve-recipe-token.d.ts +38 -0
- package/lib/utils/resolve-recipe-token.d.ts.map +1 -0
- package/lib/utils/resolve-recipe-token.js +49 -0
- package/lib/utils/split-multiple-variants-props.d.ts +29 -0
- package/lib/utils/split-multiple-variants-props.d.ts.map +1 -0
- package/lib/utils/split-multiple-variants-props.js +34 -0
- package/lib/utils/styled.d.ts +83 -0
- package/lib/utils/styled.d.ts.map +1 -0
- package/lib/utils/styled.js +144 -0
- package/package.json +54 -0
- package/src/components/ActionButton/ActionButton.test.tsx +167 -0
- package/src/components/ActionButton/ActionButton.tsx +363 -0
- package/src/components/ActionButton/index.ts +1 -0
- package/src/components/AppBar/AppBar.namespace.ts +18 -0
- package/src/components/AppBar/AppBar.test.tsx +270 -0
- package/src/components/AppBar/AppBar.tsx +288 -0
- package/src/components/AppBar/context.ts +27 -0
- package/src/components/AppBar/index.ts +21 -0
- package/src/components/AppBar/useAppBar.ts +100 -0
- package/src/components/Badge/Badge.test.tsx +54 -0
- package/src/components/Badge/Badge.tsx +32 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BottomSheet/BottomSheet.namespace.ts +24 -0
- package/src/components/BottomSheet/BottomSheet.test.tsx +219 -0
- package/src/components/BottomSheet/BottomSheet.tsx +423 -0
- package/src/components/BottomSheet/index.ts +27 -0
- package/src/components/Box/Box.test.tsx +85 -0
- package/src/components/Box/Box.tsx +39 -0
- package/src/components/Box/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.namespace.ts +12 -0
- package/src/components/Checkbox/Checkbox.tsx +282 -0
- package/src/components/Checkbox/index.ts +14 -0
- package/src/components/Icon/Icon.tsx +265 -0
- package/src/components/Icon/index.ts +8 -0
- package/src/components/ProgressCircle/ProgressCircle.namespace.ts +5 -0
- package/src/components/ProgressCircle/ProgressCircle.tsx +424 -0
- package/src/components/ProgressCircle/index.ts +9 -0
- package/src/components/RadioGroup/RadioGroup.namespace.ts +12 -0
- package/src/components/RadioGroup/RadioGroup.tsx +320 -0
- package/src/components/RadioGroup/index.ts +14 -0
- package/src/components/Stack/Stack.tsx +97 -0
- package/src/components/Stack/index.ts +1 -0
- package/src/components/Switch/Switch.namespace.ts +10 -0
- package/src/components/Switch/Switch.tsx +200 -0
- package/src/components/Switch/index.ts +12 -0
- package/src/components/TagGroup/TagGroup.namespace.ts +8 -0
- package/src/components/TagGroup/TagGroup.tsx +127 -0
- package/src/components/TagGroup/index.ts +10 -0
- package/src/components/Text/Text.tsx +64 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/index.ts +13 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useControllableState.test.ts +120 -0
- package/src/hooks/useControllableState.ts +38 -0
- package/src/hooks/useIconColor.test.ts +13 -0
- package/src/hooks/useIconColor.ts +67 -0
- package/src/hooks/usePressTap.test.ts +129 -0
- package/src/hooks/usePressTap.ts +58 -0
- package/src/hooks/useSafeArea.test.ts +83 -0
- package/src/hooks/useSafeArea.ts +56 -0
- package/src/index.ts +4 -0
- package/src/types.ts +76 -0
- package/src/utils/children.ts +13 -0
- package/src/utils/create-slot-recipe-context.test.tsx +96 -0
- package/src/utils/create-slot-recipe-context.tsx +187 -0
- package/src/utils/get-seed-class-name.ts +47 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/resolve-recipe-token.ts +47 -0
- package/src/utils/split-multiple-variants-props.test.ts +61 -0
- package/src/utils/split-multiple-variants-props.ts +59 -0
- package/src/utils/styled.ts +442 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as React from "@lynx-js/react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { tagGroup, type TagGroupVariantProps } from "@seed-design/lynx-css/recipes/tag-group";
|
|
4
|
+
import {
|
|
5
|
+
tagGroupItem,
|
|
6
|
+
type TagGroupItemVariantProps,
|
|
7
|
+
} from "@seed-design/lynx-css/recipes/tag-group-item";
|
|
8
|
+
|
|
9
|
+
import type { LynxStyledElementProps, LynxTextRef, LynxViewRef } from "../../types";
|
|
10
|
+
import { toArray } from "../../utils/children";
|
|
11
|
+
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
12
|
+
import { splitMultipleVariantsProps } from "../../utils/split-multiple-variants-props";
|
|
13
|
+
import { useStyleProps, type StyleProps } from "../../utils/styled";
|
|
14
|
+
|
|
15
|
+
const { PropsProvider, useProps, ClassNamesProvider, useClassNames } =
|
|
16
|
+
createSlotRecipeContext(tagGroupItem);
|
|
17
|
+
|
|
18
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @platform Lynx
|
|
22
|
+
*
|
|
23
|
+
* 웹 대비 미지원 기능:
|
|
24
|
+
* - `truncate` prop: Lynx flex 모델에서는 item 단위 wrap만 가능해 웹 수준의
|
|
25
|
+
* "한 줄 유지 + label ellipsis" 조합을 재현할 수 없음
|
|
26
|
+
* - `asChild` prop: Lynx Slot 미지원
|
|
27
|
+
* - 아이콘 slot: Lynx 3.7 SVG 지원 후 검토 (Tier B)
|
|
28
|
+
*/
|
|
29
|
+
export interface TagGroupRootProps
|
|
30
|
+
extends TagGroupVariantProps,
|
|
31
|
+
TagGroupItemVariantProps,
|
|
32
|
+
LynxStyledElementProps {
|
|
33
|
+
/**
|
|
34
|
+
* children 사이에 삽입되는 구분자. 기본값 `" · "`.
|
|
35
|
+
*/
|
|
36
|
+
separator?: React.ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const TagGroupRoot = React.forwardRef<unknown, TagGroupRootProps>((props, ref) => {
|
|
40
|
+
const [{ tagGroup: tagGroupVariantProps, tagGroupItem: tagGroupItemVariantProps }, otherProps] =
|
|
41
|
+
splitMultipleVariantsProps(props, { tagGroup, tagGroupItem });
|
|
42
|
+
const { children, className, separator = " · ", ...nativeProps } = otherProps;
|
|
43
|
+
const classes = tagGroup(tagGroupVariantProps);
|
|
44
|
+
|
|
45
|
+
const visibleChildren = toArray(children);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<PropsProvider value={tagGroupItemVariantProps}>
|
|
49
|
+
<view
|
|
50
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
51
|
+
className={clsx(classes.root, className)}
|
|
52
|
+
{...nativeProps}
|
|
53
|
+
>
|
|
54
|
+
{visibleChildren.map((child, index) => {
|
|
55
|
+
if (index === 0) return child;
|
|
56
|
+
// Lynx <text> separator는 개별 flex item이라 wrap이 발동하면 separator가
|
|
57
|
+
// 이전 row 끝에 남는다. separator와 뒤따르는 item을 하나의 flex-row wrapper로
|
|
58
|
+
// 묶어 같은 wrap 단위로 함께 이동시킨다.
|
|
59
|
+
return (
|
|
60
|
+
<view
|
|
61
|
+
key={(child as React.ReactElement).key ?? index}
|
|
62
|
+
className={classes.separatorWrapper}
|
|
63
|
+
>
|
|
64
|
+
<text className={classes.separator}>{separator}</text>
|
|
65
|
+
{child}
|
|
66
|
+
</view>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</view>
|
|
70
|
+
</PropsProvider>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
TagGroupRoot.displayName = "TagGroupRoot";
|
|
74
|
+
|
|
75
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @platform Lynx
|
|
79
|
+
*
|
|
80
|
+
* 웹 대비 미지원 기능:
|
|
81
|
+
* - `asChild` prop: Lynx Slot 미지원
|
|
82
|
+
*/
|
|
83
|
+
export interface TagGroupItemProps
|
|
84
|
+
extends TagGroupItemVariantProps,
|
|
85
|
+
Pick<StyleProps, "flexShrink">,
|
|
86
|
+
LynxStyledElementProps {}
|
|
87
|
+
|
|
88
|
+
export const TagGroupItem = React.forwardRef<unknown, TagGroupItemProps>((props, ref) => {
|
|
89
|
+
const parentVariantProps = useProps();
|
|
90
|
+
const [localVariantProps, otherProps] = tagGroupItem.splitVariantProps(props);
|
|
91
|
+
const classes = tagGroupItem({ ...parentVariantProps, ...localVariantProps });
|
|
92
|
+
const { style, restProps } = useStyleProps(otherProps);
|
|
93
|
+
const { children, className, ...nativeProps } = restProps;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ClassNamesProvider value={classes}>
|
|
97
|
+
<view
|
|
98
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
99
|
+
className={clsx(classes.root, className)}
|
|
100
|
+
style={style}
|
|
101
|
+
{...nativeProps}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</view>
|
|
105
|
+
</ClassNamesProvider>
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
TagGroupItem.displayName = "TagGroupItem";
|
|
109
|
+
|
|
110
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
111
|
+
|
|
112
|
+
export interface TagGroupItemLabelProps extends LynxStyledElementProps {}
|
|
113
|
+
|
|
114
|
+
export const TagGroupItemLabel = React.forwardRef<unknown, TagGroupItemLabelProps>((props, ref) => {
|
|
115
|
+
const classes = useClassNames();
|
|
116
|
+
const { children, className, ...nativeProps } = props;
|
|
117
|
+
return (
|
|
118
|
+
<text
|
|
119
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
120
|
+
className={clsx(classes.label, className)}
|
|
121
|
+
{...nativeProps}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</text>
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
TagGroupItemLabel.displayName = "TagGroupItemLabel";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { typography } from "@seed-design/lynx-css/vars/component";
|
|
2
|
+
import type { CSSProperties } from "@lynx-js/types";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import * as React from "@lynx-js/react";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
handleColor,
|
|
8
|
+
handleFontSize,
|
|
9
|
+
handleFontWeight,
|
|
10
|
+
handleLineHeight,
|
|
11
|
+
type TextStyle,
|
|
12
|
+
type TextStyleProps,
|
|
13
|
+
} from "../../utils/styled";
|
|
14
|
+
import type { LynxStyledElementProps, LynxTextRef } from "../../types";
|
|
15
|
+
|
|
16
|
+
function capitalize<T extends string>(value: T): Capitalize<T> {
|
|
17
|
+
return (value.charAt(0).toUpperCase() + value.slice(1)) as Capitalize<T>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getTypographyStyle(textStyle: TextStyle | undefined) {
|
|
21
|
+
const key = `textStyle${capitalize(textStyle ?? "t5Regular")}`;
|
|
22
|
+
const value = typography[key as keyof typeof typography];
|
|
23
|
+
return value.enabled.root;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TextProps extends TextStyleProps, LynxStyledElementProps {}
|
|
27
|
+
|
|
28
|
+
export const Text = React.forwardRef<unknown, TextProps>((props, ref) => {
|
|
29
|
+
const {
|
|
30
|
+
color,
|
|
31
|
+
textStyle,
|
|
32
|
+
fontSize,
|
|
33
|
+
lineHeight,
|
|
34
|
+
fontWeight,
|
|
35
|
+
align,
|
|
36
|
+
children,
|
|
37
|
+
className,
|
|
38
|
+
style,
|
|
39
|
+
...nativeProps
|
|
40
|
+
} = props;
|
|
41
|
+
const typographyStyle = getTypographyStyle(textStyle);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<text
|
|
45
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
46
|
+
{...nativeProps}
|
|
47
|
+
className={clsx(className)}
|
|
48
|
+
style={
|
|
49
|
+
{
|
|
50
|
+
color: handleColor(color),
|
|
51
|
+
fontSize: handleFontSize(fontSize) ?? typographyStyle.fontSize,
|
|
52
|
+
lineHeight: handleLineHeight(lineHeight ?? fontSize) ?? typographyStyle.lineHeight,
|
|
53
|
+
fontWeight: handleFontWeight(fontWeight) ?? typographyStyle.fontWeight,
|
|
54
|
+
textAlign: align,
|
|
55
|
+
...style,
|
|
56
|
+
} as CSSProperties
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</text>
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
Text.displayName = "Text";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Text, type TextProps } from "./Text";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./ActionButton";
|
|
2
|
+
export * from "./AppBar";
|
|
3
|
+
export * from "./Badge";
|
|
4
|
+
export * from "./Box";
|
|
5
|
+
export * from "./BottomSheet";
|
|
6
|
+
export * from "./Checkbox";
|
|
7
|
+
export * from "./Icon";
|
|
8
|
+
export * from "./ProgressCircle";
|
|
9
|
+
export * from "./RadioGroup";
|
|
10
|
+
export * from "./Stack";
|
|
11
|
+
export * from "./Switch";
|
|
12
|
+
export * from "./TagGroup";
|
|
13
|
+
export * from "./Text";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { act, renderHook } from "@lynx-js/react/testing-library";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { useControllableState } from "./useControllableState";
|
|
4
|
+
|
|
5
|
+
describe("useControllableState", () => {
|
|
6
|
+
describe("uncontrolled mode", () => {
|
|
7
|
+
it("initializes with defaultValue", () => {
|
|
8
|
+
const { result } = renderHook(() => useControllableState({ defaultValue: "initial" }));
|
|
9
|
+
|
|
10
|
+
expect(result.current[0]).toBe("initial");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("updates value on setValue", () => {
|
|
14
|
+
const { result } = renderHook(() => useControllableState({ defaultValue: 0 }));
|
|
15
|
+
|
|
16
|
+
act(() => {
|
|
17
|
+
result.current[1](42);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(result.current[0]).toBe(42);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("calls onChange when value changes", () => {
|
|
24
|
+
const onChange = vi.fn();
|
|
25
|
+
const { result } = renderHook(() => useControllableState({ defaultValue: "a", onChange }));
|
|
26
|
+
|
|
27
|
+
act(() => {
|
|
28
|
+
result.current[1]("b");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(onChange).toHaveBeenCalledWith("b");
|
|
32
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("does not call onChange when value is unchanged", () => {
|
|
36
|
+
const onChange = vi.fn();
|
|
37
|
+
const { result } = renderHook(() => useControllableState({ defaultValue: "a", onChange }));
|
|
38
|
+
|
|
39
|
+
act(() => {
|
|
40
|
+
result.current[1]("a");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("calls onChange only once per unique value", () => {
|
|
47
|
+
const onChange = vi.fn();
|
|
48
|
+
const { result } = renderHook(() => useControllableState({ defaultValue: 0, onChange }));
|
|
49
|
+
|
|
50
|
+
act(() => {
|
|
51
|
+
result.current[1](1);
|
|
52
|
+
});
|
|
53
|
+
act(() => {
|
|
54
|
+
result.current[1](1);
|
|
55
|
+
});
|
|
56
|
+
act(() => {
|
|
57
|
+
result.current[1](2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(onChange).toHaveBeenCalledTimes(2);
|
|
61
|
+
expect(onChange).toHaveBeenNthCalledWith(1, 1);
|
|
62
|
+
expect(onChange).toHaveBeenNthCalledWith(2, 2);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("controlled mode", () => {
|
|
67
|
+
it("uses the provided value", () => {
|
|
68
|
+
const { result } = renderHook(() =>
|
|
69
|
+
useControllableState({ value: "external", defaultValue: "default" }),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(result.current[0]).toBe("external");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("calls onChange on setValue but does not update internal state", () => {
|
|
76
|
+
const onChange = vi.fn();
|
|
77
|
+
const { result } = renderHook(() =>
|
|
78
|
+
useControllableState({
|
|
79
|
+
value: "controlled",
|
|
80
|
+
defaultValue: "default",
|
|
81
|
+
onChange,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
act(() => {
|
|
86
|
+
result.current[1]("next");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(onChange).toHaveBeenCalledWith("next");
|
|
90
|
+
expect(result.current[0]).toBe("controlled");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("reflects external value changes on rerender", () => {
|
|
94
|
+
const { result, rerender } = renderHook(
|
|
95
|
+
(props: { value: string }) =>
|
|
96
|
+
useControllableState({
|
|
97
|
+
value: props.value,
|
|
98
|
+
defaultValue: "default",
|
|
99
|
+
}),
|
|
100
|
+
{ initialProps: { value: "first" } },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result.current[0]).toBe("first");
|
|
104
|
+
|
|
105
|
+
rerender({ value: "second" });
|
|
106
|
+
|
|
107
|
+
expect(result.current[0]).toBe("second");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns a stable setValue reference", () => {
|
|
112
|
+
const { result, rerender } = renderHook(() => useControllableState({ defaultValue: 0 }));
|
|
113
|
+
|
|
114
|
+
const setValueFirst = result.current[1];
|
|
115
|
+
rerender();
|
|
116
|
+
const setValueSecond = result.current[1];
|
|
117
|
+
|
|
118
|
+
expect(setValueFirst).toBe(setValueSecond);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useRef, useState } from "@lynx-js/react";
|
|
2
|
+
import { useMemoizedFn } from "@lynx-js/lynx-ui-common";
|
|
3
|
+
|
|
4
|
+
export interface UseControllableStateProps<T> {
|
|
5
|
+
value?: T;
|
|
6
|
+
defaultValue: T;
|
|
7
|
+
onChange?: (value: T) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Controlled/uncontrolled state pattern for Lynx components.
|
|
12
|
+
*
|
|
13
|
+
* - When `value` is provided, the component is controlled.
|
|
14
|
+
* - When `value` is `undefined`, the component manages its own state
|
|
15
|
+
* starting from `defaultValue`.
|
|
16
|
+
* - `onChange` fires in both modes.
|
|
17
|
+
*/
|
|
18
|
+
export function useControllableState<T>(
|
|
19
|
+
props: UseControllableStateProps<T>,
|
|
20
|
+
): [T, (value: T) => void] {
|
|
21
|
+
const { value, defaultValue, onChange } = props;
|
|
22
|
+
const isControlled = value !== undefined;
|
|
23
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
24
|
+
const currentValue = isControlled ? value : uncontrolledValue;
|
|
25
|
+
const prevValueRef = useRef(currentValue);
|
|
26
|
+
|
|
27
|
+
const setValue = useMemoizedFn((nextValue: T) => {
|
|
28
|
+
if (!isControlled) {
|
|
29
|
+
setUncontrolledValue(nextValue);
|
|
30
|
+
}
|
|
31
|
+
if (nextValue !== prevValueRef.current) {
|
|
32
|
+
onChange?.(nextValue);
|
|
33
|
+
prevValueRef.current = nextValue;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return [currentValue, setValue];
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { renderHook } from "@lynx-js/react/testing-library";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { useIconColor } from "./useIconColor";
|
|
5
|
+
|
|
6
|
+
describe("useIconColor", () => {
|
|
7
|
+
it("returns icon tint sync props", () => {
|
|
8
|
+
const { result } = renderHook(() => useIconColor([]));
|
|
9
|
+
|
|
10
|
+
expect(result.current).toHaveProperty("ref");
|
|
11
|
+
expect(result.current["main-thread:binduiappear"]).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { runOnMainThread, useEffect, useMainThreadRef } from "@lynx-js/react";
|
|
2
|
+
import type { MainThread } from "@lynx-js/types";
|
|
3
|
+
import type { DependencyList, RefObject } from "@lynx-js/react";
|
|
4
|
+
|
|
5
|
+
type IconElement = MainThread.Element & {
|
|
6
|
+
getComputedStyleProperty?: (name: string) => string;
|
|
7
|
+
getComputedCssProperty?: (name: string) => string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface UseIconColorOptions {
|
|
11
|
+
sourceRef?: RefObject<MainThread.Element>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Lynx `<image>` 의 `tint-color` attribute 에 CSS variable 문자열을 직접 넣는 경로는
|
|
15
|
+
// 안정적으로 동작하지 않는다. CSS `color` 는 computed style 로 resolved color 를 읽을 수
|
|
16
|
+
// 있으므로 main-thread 에서 한 번 읽어 `tint-color` 로 mirror 한다.
|
|
17
|
+
function syncTintColor(targetRef: RefObject<IconElement>, sourceRef?: RefObject<IconElement>) {
|
|
18
|
+
"main thread";
|
|
19
|
+
|
|
20
|
+
const target = targetRef.current;
|
|
21
|
+
if (!target) return;
|
|
22
|
+
|
|
23
|
+
const source = sourceRef?.current ?? target;
|
|
24
|
+
|
|
25
|
+
let color: string | undefined;
|
|
26
|
+
if (typeof source.getComputedStyleProperty === "function") {
|
|
27
|
+
color = source.getComputedStyleProperty("color");
|
|
28
|
+
} else if (typeof source.getComputedCssProperty === "function") {
|
|
29
|
+
color = source.getComputedCssProperty("color");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (color) {
|
|
33
|
+
target.setAttribute("tint-color", color);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Lynx `<image>` 의 tint color 를 recipe 의 CSS `color` 로부터 main-thread 에서 읽어
|
|
39
|
+
* `tint-color` attribute 로 mirror. `deps` 가 바뀌면 재동기화.
|
|
40
|
+
*
|
|
41
|
+
* ```tsx
|
|
42
|
+
* const iconColorProps = useIconColor([variant, disabled, loading]);
|
|
43
|
+
* return cloneElement(iconChild, iconColorProps);
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useIconColor(
|
|
47
|
+
deps: DependencyList,
|
|
48
|
+
options?: UseIconColorOptions,
|
|
49
|
+
): {
|
|
50
|
+
ref: RefObject<MainThread.Element>;
|
|
51
|
+
"main-thread:binduiappear": () => void;
|
|
52
|
+
} {
|
|
53
|
+
const ref = useMainThreadRef<IconElement>(null);
|
|
54
|
+
const sourceRef = options?.sourceRef as RefObject<IconElement> | undefined;
|
|
55
|
+
|
|
56
|
+
function syncOnUiAppear() {
|
|
57
|
+
"main thread";
|
|
58
|
+
syncTintColor(ref, sourceRef);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// deps 는 caller 가 책임.
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
runOnMainThread(syncTintColor)(ref, sourceRef);
|
|
64
|
+
}, deps);
|
|
65
|
+
|
|
66
|
+
return { ref, "main-thread:binduiappear": syncOnUiAppear };
|
|
67
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { act, renderHook } from "@lynx-js/react/testing-library";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { usePressTap } from "./usePressTap";
|
|
4
|
+
|
|
5
|
+
const fakeEvent = {} as Parameters<ReturnType<typeof usePressTap>["bindtap"]>[0];
|
|
6
|
+
|
|
7
|
+
describe("usePressTap", () => {
|
|
8
|
+
describe("pressed state", () => {
|
|
9
|
+
it("starts as false", () => {
|
|
10
|
+
const { result } = renderHook(() => usePressTap());
|
|
11
|
+
expect(result.current.pressed).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("becomes true on bindtouchstart", () => {
|
|
15
|
+
const { result } = renderHook(() => usePressTap());
|
|
16
|
+
|
|
17
|
+
act(() => {
|
|
18
|
+
result.current.bindtouchstart(fakeEvent);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(result.current.pressed).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("becomes false on bindtouchend", () => {
|
|
25
|
+
const { result } = renderHook(() => usePressTap());
|
|
26
|
+
|
|
27
|
+
act(() => {
|
|
28
|
+
result.current.bindtouchstart(fakeEvent);
|
|
29
|
+
});
|
|
30
|
+
act(() => {
|
|
31
|
+
result.current.bindtouchend(fakeEvent);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(result.current.pressed).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("becomes false on bindtouchcancel", () => {
|
|
38
|
+
const { result } = renderHook(() => usePressTap());
|
|
39
|
+
|
|
40
|
+
act(() => {
|
|
41
|
+
result.current.bindtouchstart(fakeEvent);
|
|
42
|
+
});
|
|
43
|
+
act(() => {
|
|
44
|
+
result.current.bindtouchcancel(fakeEvent);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.current.pressed).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("onTap", () => {
|
|
52
|
+
it("is called when bindtap fires", () => {
|
|
53
|
+
const onTap = vi.fn();
|
|
54
|
+
const { result } = renderHook(() => usePressTap({ onTap }));
|
|
55
|
+
|
|
56
|
+
act(() => {
|
|
57
|
+
result.current.bindtap(fakeEvent);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(onTap).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("can be omitted without error", () => {
|
|
64
|
+
const { result } = renderHook(() => usePressTap());
|
|
65
|
+
|
|
66
|
+
expect(() => {
|
|
67
|
+
act(() => {
|
|
68
|
+
result.current.bindtap(fakeEvent);
|
|
69
|
+
});
|
|
70
|
+
}).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("disabled", () => {
|
|
75
|
+
it("does not update pressed on bindtouchstart", () => {
|
|
76
|
+
const { result } = renderHook(() => usePressTap({ disabled: true }));
|
|
77
|
+
|
|
78
|
+
act(() => {
|
|
79
|
+
result.current.bindtouchstart(fakeEvent);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(result.current.pressed).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not call onTap on bindtap", () => {
|
|
86
|
+
const onTap = vi.fn();
|
|
87
|
+
const { result } = renderHook(() => usePressTap({ disabled: true, onTap }));
|
|
88
|
+
|
|
89
|
+
act(() => {
|
|
90
|
+
result.current.bindtap(fakeEvent);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(onTap).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("main-thread:bindtap", () => {
|
|
98
|
+
it("is included when enabled and mainThreadOnTap is provided", () => {
|
|
99
|
+
const mainThreadOnTap = () => {};
|
|
100
|
+
const { result } = renderHook(() => usePressTap({ mainThreadOnTap }));
|
|
101
|
+
|
|
102
|
+
expect(result.current["main-thread:bindtap"]).toBe(mainThreadOnTap);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("is not included when mainThreadOnTap is omitted", () => {
|
|
106
|
+
const { result } = renderHook(() => usePressTap());
|
|
107
|
+
|
|
108
|
+
expect(result.current).not.toHaveProperty("main-thread:bindtap");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("is not included when disabled is true", () => {
|
|
112
|
+
const mainThreadOnTap = () => {};
|
|
113
|
+
const { result } = renderHook(() => usePressTap({ disabled: true, mainThreadOnTap }));
|
|
114
|
+
|
|
115
|
+
expect(result.current).not.toHaveProperty("main-thread:bindtap");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns stable handler references across renders", () => {
|
|
120
|
+
const { result, rerender } = renderHook(() => usePressTap());
|
|
121
|
+
|
|
122
|
+
const firstBindtap = result.current.bindtap;
|
|
123
|
+
const firstBindtouchstart = result.current.bindtouchstart;
|
|
124
|
+
rerender();
|
|
125
|
+
|
|
126
|
+
expect(result.current.bindtap).toBe(firstBindtap);
|
|
127
|
+
expect(result.current.bindtouchstart).toBe(firstBindtouchstart);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState } from "@lynx-js/react";
|
|
2
|
+
import { useMemoizedFn } from "@lynx-js/lynx-ui-common";
|
|
3
|
+
import type { BaseTouchEvent, EventHandler, Target } from "@lynx-js/types";
|
|
4
|
+
|
|
5
|
+
import type { LynxTouchProps } from "../types";
|
|
6
|
+
|
|
7
|
+
type TouchHandler = EventHandler<BaseTouchEvent<Target>>;
|
|
8
|
+
|
|
9
|
+
export interface UsePressTapOptions {
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
onTap?: TouchHandler;
|
|
12
|
+
mainThreadOnTap?: LynxTouchProps["main-thread:bindtap"];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UsePressTapReturn {
|
|
16
|
+
pressed: boolean;
|
|
17
|
+
bindtap: TouchHandler;
|
|
18
|
+
bindtouchstart: TouchHandler;
|
|
19
|
+
bindtouchend: TouchHandler;
|
|
20
|
+
bindtouchcancel: TouchHandler;
|
|
21
|
+
"main-thread:bindtap"?: LynxTouchProps["main-thread:bindtap"];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook that provides press/tap interaction state for Lynx elements.
|
|
26
|
+
*
|
|
27
|
+
* - Tracks `pressed` state via touch events.
|
|
28
|
+
* - When `disabled` is true, the element cannot become active and no tap fires.
|
|
29
|
+
* - If the element becomes disabled during a press, the active state is cleared.
|
|
30
|
+
* - `main-thread:bindtap` is conditionally included (only when enabled and provided).
|
|
31
|
+
*/
|
|
32
|
+
export function usePressTap(options: UsePressTapOptions = {}): UsePressTapReturn {
|
|
33
|
+
const { disabled = false, onTap, mainThreadOnTap } = options;
|
|
34
|
+
const [pressed, setPressed] = useState(false);
|
|
35
|
+
|
|
36
|
+
const press = useMemoizedFn(() => {
|
|
37
|
+
if (disabled) return;
|
|
38
|
+
setPressed(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const reset = useMemoizedFn(() => {
|
|
42
|
+
setPressed(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const handleTap = useMemoizedFn((...args: Parameters<TouchHandler>) => {
|
|
46
|
+
if (disabled) return;
|
|
47
|
+
onTap?.(...args);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
pressed,
|
|
52
|
+
bindtap: handleTap,
|
|
53
|
+
bindtouchstart: press,
|
|
54
|
+
bindtouchend: reset,
|
|
55
|
+
bindtouchcancel: reset,
|
|
56
|
+
...(!disabled && mainThreadOnTap ? { "main-thread:bindtap": mainThreadOnTap } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|