@seed-design/lynx-react 0.1.1-alpha.0 → 0.2.0-alpha.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/ActionButton/ActionButton.d.ts +40 -9
- package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
- package/lib/components/ActionButton/ActionButton.jsx +132 -14
- package/lib/components/BottomSheet/BottomSheet.d.ts +92 -0
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/lib/components/BottomSheet/BottomSheet.jsx +178 -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 +1 -0
- package/lib/components/BottomSheet/index.d.ts +3 -0
- package/lib/components/BottomSheet/index.d.ts.map +1 -0
- package/lib/components/BottomSheet/index.js +2 -0
- package/lib/components/Checkbox/Checkbox.d.ts +51 -0
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/lib/components/Checkbox/Checkbox.jsx +121 -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 +1 -0
- package/lib/components/Checkbox/index.d.ts +3 -0
- package/lib/components/Checkbox/index.d.ts.map +1 -0
- package/lib/components/Checkbox/index.js +2 -0
- package/lib/components/ProgressCircle/ProgressCircle.d.ts +10 -9
- package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
- package/lib/components/ProgressCircle/ProgressCircle.jsx +8 -14
- 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 +1 -0
- package/lib/components/ProgressCircle/index.d.ts +2 -1
- package/lib/components/ProgressCircle/index.d.ts.map +1 -1
- package/lib/components/ProgressCircle/index.js +2 -1
- package/lib/components/RadioGroup/RadioGroup.d.ts +50 -0
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.jsx +144 -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 +1 -0
- package/lib/components/RadioGroup/index.d.ts +3 -0
- package/lib/components/RadioGroup/index.d.ts.map +1 -0
- package/lib/components/RadioGroup/index.js +2 -0
- package/lib/components/Switch/Switch.d.ts +27 -0
- package/lib/components/Switch/Switch.d.ts.map +1 -0
- package/lib/components/Switch/Switch.jsx +75 -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 +1 -0
- package/lib/components/Switch/index.d.ts +3 -0
- package/lib/components/Switch/index.d.ts.map +1 -0
- package/lib/components/Switch/index.js +2 -0
- package/lib/components/TagGroup/TagGroup.d.ts +40 -0
- package/lib/components/TagGroup/TagGroup.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.jsx +54 -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 +1 -0
- package/lib/components/TagGroup/index.d.ts +3 -0
- package/lib/components/TagGroup/index.d.ts.map +1 -0
- package/lib/components/TagGroup/index.js +2 -0
- package/lib/components/index.d.ts +8 -0
- package/lib/components/index.d.ts.map +1 -0
- package/lib/components/index.js +7 -0
- package/lib/hooks/index.d.ts +4 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +3 -0
- package/lib/hooks/use-controllable-state.d.ts +15 -0
- package/lib/hooks/use-controllable-state.d.ts.map +1 -0
- package/lib/hooks/use-controllable-state.js +27 -0
- package/lib/hooks/use-icon-color.d.ts +15 -0
- package/lib/hooks/use-icon-color.d.ts.map +1 -0
- package/lib/hooks/use-icon-color.js +37 -0
- package/lib/hooks/use-press-tap.d.ts +24 -0
- package/lib/hooks/use-press-tap.d.ts.map +1 -0
- package/lib/hooks/use-press-tap.js +35 -0
- package/lib/index.d.ts +3 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -4
- package/lib/utils/create-slot-recipe-context.d.ts +26 -0
- package/lib/utils/create-slot-recipe-context.d.ts.map +1 -0
- package/lib/utils/create-slot-recipe-context.jsx +76 -0
- package/lib/utils/get-seed-class-name.d.ts.map +1 -0
- package/lib/utils/index.d.ts +5 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/index.js +4 -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 +47 -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/package.json +12 -9
- package/src/components/ActionButton/ActionButton.tsx +290 -31
- package/src/components/ActionButton/__tests__/ActionButton.test.tsx +35 -0
- package/src/components/BottomSheet/BottomSheet.namespace.ts +24 -0
- package/src/components/BottomSheet/BottomSheet.tsx +386 -0
- package/src/components/BottomSheet/__tests__/BottomSheet.test.tsx +119 -0
- package/src/components/BottomSheet/index.ts +27 -0
- package/src/components/Checkbox/Checkbox.namespace.ts +12 -0
- package/src/components/Checkbox/Checkbox.tsx +291 -0
- package/src/components/Checkbox/index.ts +14 -0
- package/src/components/ProgressCircle/ProgressCircle.namespace.ts +5 -0
- package/src/components/ProgressCircle/ProgressCircle.tsx +14 -13
- package/src/components/ProgressCircle/index.ts +9 -1
- package/src/components/RadioGroup/RadioGroup.namespace.ts +12 -0
- package/src/components/RadioGroup/RadioGroup.tsx +338 -0
- package/src/components/RadioGroup/index.ts +14 -0
- package/src/components/Switch/Switch.namespace.ts +10 -0
- package/src/components/Switch/Switch.tsx +191 -0
- package/src/components/Switch/index.ts +12 -0
- package/src/components/TagGroup/TagGroup.namespace.ts +8 -0
- package/src/components/TagGroup/TagGroup.tsx +125 -0
- package/src/components/TagGroup/index.ts +10 -0
- package/src/components/__tests__/ProgressCircle.test.ts +33 -0
- package/src/components/__tests__/exports.test.ts +38 -0
- package/src/components/__tests__/switch.test.ts +34 -0
- package/src/components/index.ts +7 -0
- package/src/hooks/__tests__/use-controllable-state.test.ts +120 -0
- package/src/hooks/__tests__/use-press-tap.test.ts +129 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-controllable-state.ts +38 -0
- package/src/hooks/use-icon-color.ts +51 -0
- package/src/hooks/use-press-tap.ts +54 -0
- package/src/index.ts +3 -4
- package/src/utils/__tests__/create-slot-recipe-context.test.tsx +228 -0
- package/src/utils/__tests__/split-multiple-variants-props.test.ts +61 -0
- package/src/utils/create-slot-recipe-context.tsx +147 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/resolve-recipe-token.ts +47 -0
- package/src/utils/split-multiple-variants-props.ts +59 -0
- package/lib/components/Box/Box.d.ts +0 -10
- package/lib/components/Box/Box.d.ts.map +0 -1
- package/lib/components/Box/Box.jsx +0 -19
- package/lib/components/Box/index.d.ts +0 -2
- package/lib/components/Box/index.d.ts.map +0 -1
- package/lib/components/Box/index.js +0 -1
- package/lib/components/Flex/Flex.d.ts +0 -33
- package/lib/components/Flex/Flex.d.ts.map +0 -1
- package/lib/components/Flex/Flex.jsx +0 -7
- package/lib/components/Flex/index.d.ts +0 -2
- package/lib/components/Flex/index.d.ts.map +0 -1
- package/lib/components/Flex/index.js +0 -1
- package/lib/components/List/List.d.ts +0 -9
- package/lib/components/List/List.d.ts.map +0 -1
- package/lib/components/List/List.jsx +0 -17
- package/lib/components/List/index.d.ts +0 -2
- package/lib/components/List/index.d.ts.map +0 -1
- package/lib/components/List/index.js +0 -1
- package/lib/components/ListItem/ListItem.d.ts +0 -8
- package/lib/components/ListItem/ListItem.d.ts.map +0 -1
- package/lib/components/ListItem/ListItem.jsx +0 -11
- package/lib/components/ListItem/index.d.ts +0 -2
- package/lib/components/ListItem/index.d.ts.map +0 -1
- package/lib/components/ListItem/index.js +0 -1
- package/lib/components/ScrollView/ScrollView.d.ts +0 -9
- package/lib/components/ScrollView/ScrollView.d.ts.map +0 -1
- package/lib/components/ScrollView/ScrollView.jsx +0 -17
- package/lib/components/ScrollView/index.d.ts +0 -2
- package/lib/components/ScrollView/index.d.ts.map +0 -1
- package/lib/components/ScrollView/index.js +0 -1
- package/lib/components/Stack/Stack.d.ts +0 -6
- package/lib/components/Stack/Stack.d.ts.map +0 -1
- package/lib/components/Stack/Stack.jsx +0 -10
- package/lib/components/Stack/index.d.ts +0 -2
- package/lib/components/Stack/index.d.ts.map +0 -1
- package/lib/components/Stack/index.js +0 -1
- package/lib/components/Text/Text.d.ts +0 -30
- package/lib/components/Text/Text.d.ts.map +0 -1
- package/lib/components/Text/Text.jsx +0 -49
- package/lib/components/Text/index.d.ts +0 -2
- package/lib/components/Text/index.d.ts.map +0 -1
- package/lib/components/Text/index.js +0 -1
- package/lib/get-seed-class-name.d.ts.map +0 -1
- package/lib/utils/dynamic-style.d.ts +0 -13
- package/lib/utils/dynamic-style.d.ts.map +0 -1
- package/lib/utils/dynamic-style.js +0 -26
- package/lib/utils/styled.d.ts +0 -120
- package/lib/utils/styled.d.ts.map +0 -1
- package/lib/utils/styled.jsx +0 -131
- package/src/utils/dynamic-style.ts +0 -26
- /package/lib/{get-seed-class-name.d.ts → utils/get-seed-class-name.d.ts} +0 -0
- /package/lib/{get-seed-class-name.js → utils/get-seed-class-name.js} +0 -0
- /package/src/{get-seed-class-name.ts → utils/get-seed-class-name.ts} +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as React from "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 { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
10
|
+
import { splitMultipleVariantsProps } from "../../utils/split-multiple-variants-props";
|
|
11
|
+
|
|
12
|
+
const { PropsProvider, useProps, ClassNamesProvider, useClassNames } =
|
|
13
|
+
createSlotRecipeContext(tagGroupItem);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @platform Lynx
|
|
17
|
+
*
|
|
18
|
+
* 웹 대비 미지원 기능:
|
|
19
|
+
* - `truncate` prop: Lynx flex 모델에서는 item 단위 wrap만 가능해 웹 수준의
|
|
20
|
+
* "한 줄 유지 + label ellipsis" 조합을 재현할 수 없음
|
|
21
|
+
* - `asChild` prop: Lynx Slot 미지원
|
|
22
|
+
* - `flexShrink` prop: CSS variable 동적 주입 제한
|
|
23
|
+
* - 아이콘 slot: Lynx 3.7 SVG 지원 후 검토 (Tier B)
|
|
24
|
+
*/
|
|
25
|
+
export interface TagGroupRootProps extends TagGroupVariantProps, TagGroupItemVariantProps {
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
/**
|
|
29
|
+
* children 사이에 삽입되는 구분자. 기본값 `" · "`.
|
|
30
|
+
*/
|
|
31
|
+
separator?: React.ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const TagGroupRoot = React.forwardRef<unknown, TagGroupRootProps>((props, ref) => {
|
|
35
|
+
const [{ tagGroup: tagGroupVariantProps, tagGroupItem: tagGroupItemVariantProps }, otherProps] =
|
|
36
|
+
splitMultipleVariantsProps(props, { tagGroup, tagGroupItem });
|
|
37
|
+
const { children, className, separator = " · ", ...nativeProps } = otherProps;
|
|
38
|
+
const classes = tagGroup(tagGroupVariantProps);
|
|
39
|
+
|
|
40
|
+
const visibleChildren = React.Children.toArray(children);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<PropsProvider value={tagGroupItemVariantProps}>
|
|
44
|
+
<view
|
|
45
|
+
{...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
|
|
46
|
+
className={clsx(classes.root, className)}
|
|
47
|
+
{...nativeProps}
|
|
48
|
+
>
|
|
49
|
+
{visibleChildren.map((child, index) => {
|
|
50
|
+
if (index === 0) return child;
|
|
51
|
+
// Lynx <text> separator는 개별 flex item이라 wrap이 발동하면 separator가
|
|
52
|
+
// 이전 row 끝에 남는다. separator와 뒤따르는 item을 하나의 flex-row wrapper로
|
|
53
|
+
// 묶어 같은 wrap 단위로 함께 이동시킨다.
|
|
54
|
+
return (
|
|
55
|
+
<view
|
|
56
|
+
key={(child as React.ReactElement).key ?? index}
|
|
57
|
+
style={{
|
|
58
|
+
display: "flex",
|
|
59
|
+
flexDirection: "row",
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
flexShrink: 0,
|
|
62
|
+
}}
|
|
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
|
+
* @platform Lynx
|
|
77
|
+
*
|
|
78
|
+
* 웹 대비 미지원 기능:
|
|
79
|
+
* - `asChild` prop: Lynx Slot 미지원
|
|
80
|
+
* - `flexShrink` prop: CSS variable 동적 주입 제한
|
|
81
|
+
*/
|
|
82
|
+
export interface TagGroupItemProps extends TagGroupItemVariantProps {
|
|
83
|
+
children?: React.ReactNode;
|
|
84
|
+
className?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const TagGroupItem = React.forwardRef<unknown, TagGroupItemProps>((props, ref) => {
|
|
88
|
+
const parentVariantProps = useProps();
|
|
89
|
+
const [localVariantProps, otherProps] = tagGroupItem.splitVariantProps(props);
|
|
90
|
+
const classes = tagGroupItem({ ...parentVariantProps, ...localVariantProps });
|
|
91
|
+
const { children, className, ...nativeProps } = otherProps;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<ClassNamesProvider value={classes}>
|
|
95
|
+
<view
|
|
96
|
+
{...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
|
|
97
|
+
className={clsx(classes.root, className)}
|
|
98
|
+
{...nativeProps}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</view>
|
|
102
|
+
</ClassNamesProvider>
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
TagGroupItem.displayName = "TagGroupItem";
|
|
106
|
+
|
|
107
|
+
export interface TagGroupItemLabelProps {
|
|
108
|
+
children?: React.ReactNode;
|
|
109
|
+
className?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const TagGroupItemLabel = React.forwardRef<unknown, TagGroupItemLabelProps>((props, ref) => {
|
|
113
|
+
const classes = useClassNames();
|
|
114
|
+
const { children, className, ...nativeProps } = props;
|
|
115
|
+
return (
|
|
116
|
+
<text
|
|
117
|
+
{...(ref ? { ref: ref as React.Ref<SVGTextElement> } : {})}
|
|
118
|
+
className={clsx(classes.label, className)}
|
|
119
|
+
{...nativeProps}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</text>
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
TagGroupItemLabel.displayName = "TagGroupItemLabel";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const progressCircleSource = readFileSync(
|
|
8
|
+
join(currentDir, "..", "ProgressCircle", "ProgressCircle.tsx"),
|
|
9
|
+
"utf8",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
function readEffectDependencies(marker: string): string {
|
|
13
|
+
const markerIndex = progressCircleSource.indexOf(marker);
|
|
14
|
+
expect(markerIndex).toBeGreaterThanOrEqual(0);
|
|
15
|
+
|
|
16
|
+
const dependencyStart = progressCircleSource.indexOf("}, [", markerIndex);
|
|
17
|
+
expect(dependencyStart).toBeGreaterThanOrEqual(0);
|
|
18
|
+
|
|
19
|
+
const dependencyEnd = progressCircleSource.indexOf("]);", dependencyStart);
|
|
20
|
+
expect(dependencyEnd).toBeGreaterThanOrEqual(0);
|
|
21
|
+
|
|
22
|
+
return progressCircleSource.slice(dependencyStart, dependencyEnd);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("ProgressCircle", () => {
|
|
26
|
+
it("restarts indeterminate animation when size changes", () => {
|
|
27
|
+
expect(readEffectDependencies("runOnMainThread(startLoop)")).toContain("numSize");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("restarts determinate transition when size changes", () => {
|
|
31
|
+
expect(readEffectDependencies("runOnMainThread(startAnimation)")).toContain("numSize");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const componentsDir = join(currentDir, "..");
|
|
8
|
+
|
|
9
|
+
function readComponentFile(componentName: string, fileName: string): string {
|
|
10
|
+
return readFileSync(join(componentsDir, componentName, fileName), "utf8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("components public exports", () => {
|
|
14
|
+
it("exposes compound components through namespace exports", () => {
|
|
15
|
+
for (const componentName of ["Switch", "Checkbox", "RadioGroup", "TagGroup"]) {
|
|
16
|
+
expect(readComponentFile(componentName, "index.ts")).toContain(
|
|
17
|
+
`export * as ${componentName} from "./${componentName}.namespace";`,
|
|
18
|
+
);
|
|
19
|
+
expect(readComponentFile(componentName, `${componentName}.namespace.ts`)).toContain(
|
|
20
|
+
" as Root",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("keeps ProgressCircle namespace usage while exposing flat slots", () => {
|
|
26
|
+
const index = readComponentFile("ProgressCircle", "index.ts");
|
|
27
|
+
const implementation = readComponentFile("ProgressCircle", "ProgressCircle.tsx");
|
|
28
|
+
const namespace = readComponentFile("ProgressCircle", "ProgressCircle.namespace.ts");
|
|
29
|
+
|
|
30
|
+
expect(index).toContain('export * as ProgressCircle from "./ProgressCircle.namespace";');
|
|
31
|
+
expect(index).toContain("ProgressCircleRoot");
|
|
32
|
+
expect(index).toContain("ProgressCircleRange");
|
|
33
|
+
expect(namespace).toContain("ProgressCircleRoot as Root");
|
|
34
|
+
expect(namespace).toContain("ProgressCircleRange as Range");
|
|
35
|
+
expect(implementation).toContain("export const ProgressCircleRoot");
|
|
36
|
+
expect(implementation).toContain("export const ProgressCircleRange");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { switchmark } from "@seed-design/lynx-css/recipes/switchmark";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const lynxCssRecipesDir = join(currentDir, "..", "..", "..", "..", "lynx-css", "recipes");
|
|
9
|
+
const switchCss = readFileSync(join(lynxCssRecipesDir, "switch.css"), "utf8");
|
|
10
|
+
const switchmarkCss = readFileSync(join(lynxCssRecipesDir, "switchmark.css"), "utf8");
|
|
11
|
+
|
|
12
|
+
describe("Switch", () => {
|
|
13
|
+
it.each(["16", "24", "32"] as const)(
|
|
14
|
+
"includes selected thumb class for size %s",
|
|
15
|
+
(size) => {
|
|
16
|
+
const classNames = switchmark({
|
|
17
|
+
tone: "brand",
|
|
18
|
+
size,
|
|
19
|
+
checked: true,
|
|
20
|
+
disabled: false,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(classNames.thumb.split(" ")).toContain(
|
|
24
|
+
`seed-switchmark__thumb--size_${size}-checked_true`,
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
it("aligns Lynx switch content with flex center instead of margin compensation", () => {
|
|
30
|
+
expect(switchCss).toContain("align-items: center");
|
|
31
|
+
expect(switchCss).not.toContain("--switchmark-margin-top");
|
|
32
|
+
expect(switchmarkCss).not.toContain("--switchmark-margin-top");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -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 "../use-controllable-state";
|
|
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,129 @@
|
|
|
1
|
+
import { act, renderHook } from "@lynx-js/react/testing-library";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { usePressTap } from "../use-press-tap";
|
|
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,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,51 @@
|
|
|
1
|
+
import { runOnMainThread, useEffect, useMainThreadRef } from "@lynx-js/react";
|
|
2
|
+
import type { MainThread } from "@lynx-js/types";
|
|
3
|
+
import type { DependencyList, RefObject } from "react";
|
|
4
|
+
|
|
5
|
+
type IconElement = MainThread.Element & {
|
|
6
|
+
getComputedStyleProperty?: (name: string) => string;
|
|
7
|
+
getComputedCssProperty?: (name: string) => string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Lynx `<image>` 의 `tint-color` attribute 는 concrete color(hex/rgb) 만 받는다. CSS
|
|
11
|
+
// `color: var(--seed-color-...)` 를 slot 에 걸면 `getComputedStyleProperty("color")` 가
|
|
12
|
+
// resolved hex 를 돌려주므로 main-thread 에서 한번 읽어 `tint-color` 로 mirror 한다.
|
|
13
|
+
function syncTintColor(ref: RefObject<IconElement>) {
|
|
14
|
+
"main thread";
|
|
15
|
+
|
|
16
|
+
const el = ref.current;
|
|
17
|
+
if (!el) return;
|
|
18
|
+
|
|
19
|
+
let color: string | undefined;
|
|
20
|
+
if (typeof el.getComputedStyleProperty === "function") {
|
|
21
|
+
color = el.getComputedStyleProperty("color");
|
|
22
|
+
} else if (typeof el.getComputedCssProperty === "function") {
|
|
23
|
+
color = el.getComputedCssProperty("color");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (color) {
|
|
27
|
+
el.setAttribute("tint-color", color);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lynx `<image>` 의 tint color 를 recipe 의 CSS `color` 로부터 main-thread 에서 읽어
|
|
33
|
+
* `tint-color` attribute 로 mirror. `deps` 가 바뀌면 재동기화.
|
|
34
|
+
*
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const { ref } = useIconColor([variant, disabled, loading]);
|
|
37
|
+
* return cloneElement(iconChild, { ref });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useIconColor(deps: DependencyList): {
|
|
41
|
+
ref: RefObject<MainThread.Element>;
|
|
42
|
+
} {
|
|
43
|
+
const ref = useMainThreadRef<IconElement>(null);
|
|
44
|
+
|
|
45
|
+
// deps 는 caller 가 책임.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
runOnMainThread(syncTintColor)(ref);
|
|
48
|
+
}, deps);
|
|
49
|
+
|
|
50
|
+
return { ref };
|
|
51
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useState } from "@lynx-js/react";
|
|
2
|
+
import type { TouchEvent } from "@lynx-js/types";
|
|
3
|
+
import { useMemoizedFn } from "@lynx-js/lynx-ui-common";
|
|
4
|
+
|
|
5
|
+
export interface UsePressTapOptions {
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onTap?: () => void;
|
|
8
|
+
mainThreadOnTap?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UsePressTapReturn {
|
|
12
|
+
pressed: boolean;
|
|
13
|
+
bindtap: (e: TouchEvent) => void;
|
|
14
|
+
bindtouchstart: (e: TouchEvent) => void;
|
|
15
|
+
bindtouchend: (e: TouchEvent) => void;
|
|
16
|
+
bindtouchcancel: (e: TouchEvent) => void;
|
|
17
|
+
"main-thread:bindtap"?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook that provides press/tap interaction state for Lynx elements.
|
|
22
|
+
*
|
|
23
|
+
* - Tracks `pressed` state via touch events.
|
|
24
|
+
* - When `disabled` is true, the element cannot become active and no tap fires.
|
|
25
|
+
* - If the element becomes disabled during a press, the active state is cleared.
|
|
26
|
+
* - `main-thread:bindtap` is conditionally included (only when enabled and provided).
|
|
27
|
+
*/
|
|
28
|
+
export function usePressTap(options: UsePressTapOptions = {}): UsePressTapReturn {
|
|
29
|
+
const { disabled = false, onTap, mainThreadOnTap } = options;
|
|
30
|
+
const [pressed, setPressed] = useState(false);
|
|
31
|
+
|
|
32
|
+
const press = useMemoizedFn(() => {
|
|
33
|
+
if (disabled) return;
|
|
34
|
+
setPressed(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const reset = useMemoizedFn(() => {
|
|
38
|
+
setPressed(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const handleTap = useMemoizedFn(() => {
|
|
42
|
+
if (disabled) return;
|
|
43
|
+
onTap?.();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
pressed,
|
|
48
|
+
bindtap: handleTap,
|
|
49
|
+
bindtouchstart: press,
|
|
50
|
+
bindtouchend: reset,
|
|
51
|
+
bindtouchcancel: reset,
|
|
52
|
+
...(!disabled && mainThreadOnTap ? { "main-thread:bindtap": mainThreadOnTap } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export { getSeedClassName } from "./get-seed-class-name";
|
|
1
|
+
export * from "./components";
|
|
2
|
+
export * from "./hooks";
|
|
3
|
+
export * from "./utils";
|