@seed-design/lynx-react 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Accordion/Accordion.d.ts +56 -0
- package/lib/components/Accordion/Accordion.d.ts.map +1 -0
- package/lib/components/Accordion/Accordion.jsx +210 -0
- package/lib/components/Accordion/Accordion.namespace.d.ts +2 -0
- package/lib/components/Accordion/Accordion.namespace.d.ts.map +1 -0
- package/lib/components/Accordion/Accordion.namespace.js +17 -0
- package/lib/components/Accordion/index.d.ts +3 -0
- package/lib/components/Accordion/index.d.ts.map +1 -0
- package/lib/components/ActionButton/ActionButton.jsx +6 -6
- package/lib/components/AppBar/AppBar.jsx +1 -1
- package/lib/components/Badge/Badge.jsx +1 -1
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.jsx +6 -3
- package/lib/components/Box/Box.jsx +1 -1
- package/lib/components/Callout/Callout.d.ts +32 -0
- package/lib/components/Callout/Callout.d.ts.map +1 -0
- package/lib/components/Callout/Callout.jsx +113 -0
- package/lib/components/Callout/Callout.namespace.d.ts +2 -0
- package/lib/components/Callout/Callout.namespace.d.ts.map +1 -0
- package/lib/components/Callout/Callout.namespace.js +13 -0
- package/lib/components/Callout/index.d.ts +3 -0
- package/lib/components/Callout/index.d.ts.map +1 -0
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/lib/components/Checkbox/Checkbox.jsx +36 -5
- package/lib/components/Chip/Chip.d.ts +66 -0
- package/lib/components/Chip/Chip.d.ts.map +1 -0
- package/lib/components/Chip/Chip.jsx +159 -0
- package/lib/components/Chip/Chip.namespace.d.ts +2 -0
- package/lib/components/Chip/Chip.namespace.d.ts.map +1 -0
- package/lib/components/Chip/Chip.namespace.js +16 -0
- package/lib/components/Chip/index.d.ts +3 -0
- package/lib/components/Chip/index.d.ts.map +1 -0
- package/lib/components/Divider/Divider.d.ts +31 -0
- package/lib/components/Divider/Divider.d.ts.map +1 -0
- package/lib/components/Divider/Divider.jsx +26 -0
- package/lib/components/Divider/index.d.ts +2 -0
- package/lib/components/Divider/index.d.ts.map +1 -0
- package/lib/components/Field/Field.jsx +1 -1
- package/lib/components/Icon/Icon.jsx +2 -2
- package/lib/components/ProgressCircle/ProgressCircle.jsx +2 -2
- package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
- package/lib/components/Stack/Stack.jsx +1 -1
- package/lib/components/Switch/Switch.jsx +2 -2
- package/lib/components/Tabs/Tabs.d.ts +59 -0
- package/lib/components/Tabs/Tabs.d.ts.map +1 -0
- package/lib/components/Tabs/Tabs.jsx +419 -0
- package/lib/components/Tabs/Tabs.namespace.d.ts +2 -0
- package/lib/components/Tabs/Tabs.namespace.d.ts.map +1 -0
- package/lib/components/Tabs/Tabs.namespace.js +14 -0
- package/lib/components/Tabs/Tabs.utils.d.ts +18 -0
- package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -0
- package/lib/components/Tabs/Tabs.utils.js +37 -0
- package/lib/components/Tabs/index.d.ts +3 -0
- package/lib/components/Tabs/index.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.jsx +2 -2
- package/lib/components/Text/Text.jsx +2 -2
- package/lib/components/TextField/TextField.jsx +2 -2
- package/lib/components/index.d.ts +5 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/hooks/useIconColor.d.ts +2 -1
- package/lib/hooks/useIconColor.d.ts.map +1 -1
- package/lib/hooks/useIconColor.js +29 -5
- package/lib/hooks/usePressTap.d.ts.map +1 -1
- package/lib/hooks/usePressTap.js +1 -0
- package/lib/index.js +14 -5
- package/lib/utils/create-slot-recipe-context.jsx +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.namespace.ts +22 -0
- package/src/components/Accordion/Accordion.test.tsx +129 -0
- package/src/components/Accordion/Accordion.tsx +453 -0
- package/src/components/Accordion/index.ts +24 -0
- package/src/components/BottomSheet/BottomSheet.test.tsx +24 -0
- package/src/components/BottomSheet/BottomSheet.tsx +14 -2
- package/src/components/Callout/Callout.namespace.ts +14 -0
- package/src/components/Callout/Callout.test.tsx +188 -0
- package/src/components/Callout/Callout.tsx +272 -0
- package/src/components/Callout/index.ts +16 -0
- package/src/components/Checkbox/Checkbox.test.tsx +176 -0
- package/src/components/Checkbox/Checkbox.tsx +36 -2
- package/src/components/Chip/Chip.namespace.ts +20 -0
- package/src/components/Chip/Chip.test.tsx +113 -0
- package/src/components/Chip/Chip.tsx +373 -0
- package/src/components/Chip/index.ts +22 -0
- package/src/components/Divider/Divider.test.tsx +73 -0
- package/src/components/Divider/Divider.tsx +76 -0
- package/src/components/Divider/index.ts +1 -0
- package/src/components/Icon/Icon.test.tsx +129 -0
- package/src/components/Tabs/Tabs.namespace.ts +16 -0
- package/src/components/Tabs/Tabs.test.tsx +221 -0
- package/src/components/Tabs/Tabs.tsx +727 -0
- package/src/components/Tabs/Tabs.utils.ts +56 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/index.ts +5 -0
- package/src/hooks/useIconColor.ts +43 -5
- package/src/hooks/usePressTap.test.ts +13 -0
- package/src/hooks/usePressTap.ts +1 -0
|
@@ -38,6 +38,8 @@ interface CheckboxContextValue {
|
|
|
38
38
|
indeterminate: boolean;
|
|
39
39
|
disabled: boolean;
|
|
40
40
|
pressed: boolean;
|
|
41
|
+
pressStartChecked: boolean;
|
|
42
|
+
pressStartIndeterminate: boolean;
|
|
41
43
|
checkboxVariantProps: CheckboxVariantProps;
|
|
42
44
|
checkmarkVariantProps: CheckmarkVariantProps;
|
|
43
45
|
toggle: () => void;
|
|
@@ -103,10 +105,18 @@ export const CheckboxRoot = React.forwardRef<unknown, CheckboxRootProps>((props,
|
|
|
103
105
|
|
|
104
106
|
const toggle = React.useCallback(() => setChecked(!checked), [checked, setChecked]);
|
|
105
107
|
|
|
106
|
-
const {
|
|
108
|
+
const pressSelectionRef = React.useRef({ checked, indeterminate });
|
|
109
|
+
const { pressed, bindtouchstart, ...pressHandlers } = usePressTap({
|
|
107
110
|
disabled,
|
|
108
111
|
onTap: toggle,
|
|
109
112
|
});
|
|
113
|
+
const handleTouchStart = React.useCallback(
|
|
114
|
+
(...args: Parameters<typeof bindtouchstart>) => {
|
|
115
|
+
pressSelectionRef.current = { checked, indeterminate };
|
|
116
|
+
bindtouchstart(...args);
|
|
117
|
+
},
|
|
118
|
+
[bindtouchstart, checked, indeterminate],
|
|
119
|
+
);
|
|
110
120
|
|
|
111
121
|
const rootClassName = checkbox({ ...checkboxVariantProps, disabled }).root;
|
|
112
122
|
|
|
@@ -116,6 +126,8 @@ export const CheckboxRoot = React.forwardRef<unknown, CheckboxRootProps>((props,
|
|
|
116
126
|
indeterminate,
|
|
117
127
|
disabled,
|
|
118
128
|
pressed,
|
|
129
|
+
pressStartChecked: pressSelectionRef.current.checked,
|
|
130
|
+
pressStartIndeterminate: pressSelectionRef.current.indeterminate,
|
|
119
131
|
checkboxVariantProps,
|
|
120
132
|
checkmarkVariantProps,
|
|
121
133
|
toggle,
|
|
@@ -136,6 +148,7 @@ export const CheckboxRoot = React.forwardRef<unknown, CheckboxRootProps>((props,
|
|
|
136
148
|
<view
|
|
137
149
|
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
138
150
|
className={clsx(rootClassName, className)}
|
|
151
|
+
bindtouchstart={handleTouchStart}
|
|
139
152
|
{...pressHandlers}
|
|
140
153
|
{...nativeProps}
|
|
141
154
|
>
|
|
@@ -166,15 +179,36 @@ export const CheckboxControl = React.forwardRef<unknown, CheckboxControlProps>((
|
|
|
166
179
|
};
|
|
167
180
|
const classes = checkmark(checkmarkVariantProps);
|
|
168
181
|
|
|
182
|
+
// Lynx는 opaque color와 transparent black 사이의 background-color를 보간할 때
|
|
183
|
+
// 중간 RGB가 검게 탁해진다. ghost root는 투명 상태로 고정하고 별도 배경의
|
|
184
|
+
// opacity만 전환한다.
|
|
185
|
+
const pressStartClasses = checkmark({
|
|
186
|
+
...checkmarkVariantProps,
|
|
187
|
+
checked: context.pressStartChecked,
|
|
188
|
+
indeterminate: context.pressStartIndeterminate,
|
|
189
|
+
pressed: context.pressed,
|
|
190
|
+
});
|
|
191
|
+
const isGhost = checkmarkVariantProps.variant === "ghost";
|
|
192
|
+
// ghost root 상태를 고정하고 overlay opacity만 전환한다.
|
|
193
|
+
const rootClassName = isGhost
|
|
194
|
+
? checkmark({
|
|
195
|
+
...checkmarkVariantProps,
|
|
196
|
+
checked: false,
|
|
197
|
+
indeterminate: false,
|
|
198
|
+
pressed: false,
|
|
199
|
+
}).root
|
|
200
|
+
: classes.root;
|
|
201
|
+
|
|
169
202
|
return (
|
|
170
203
|
<CheckmarkControlContext.Provider
|
|
171
204
|
value={{ iconClassName: classes.icon, checkmarkVariantProps }}
|
|
172
205
|
>
|
|
173
206
|
<view
|
|
174
207
|
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
175
|
-
className={clsx(
|
|
208
|
+
className={clsx(rootClassName, className)}
|
|
176
209
|
{...nativeProps}
|
|
177
210
|
>
|
|
211
|
+
{isGhost ? <view className={pressStartClasses.background} /> : null}
|
|
178
212
|
{children}
|
|
179
213
|
</view>
|
|
180
214
|
</CheckmarkControlContext.Provider>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ChipButton as Button,
|
|
3
|
+
ChipLabel as Label,
|
|
4
|
+
ChipPrefixAvatar as PrefixAvatar,
|
|
5
|
+
ChipPrefixIcon as PrefixIcon,
|
|
6
|
+
ChipRadioItem as RadioItem,
|
|
7
|
+
ChipRadioRoot as RadioRoot,
|
|
8
|
+
ChipRoot as Root,
|
|
9
|
+
ChipSuffixIcon as SuffixIcon,
|
|
10
|
+
ChipToggle as Toggle,
|
|
11
|
+
type ChipButtonProps as ButtonProps,
|
|
12
|
+
type ChipLabelProps as LabelProps,
|
|
13
|
+
type ChipPrefixAvatarProps as PrefixAvatarProps,
|
|
14
|
+
type ChipPrefixIconProps as PrefixIconProps,
|
|
15
|
+
type ChipRadioItemProps as RadioItemProps,
|
|
16
|
+
type ChipRadioRootProps as RadioRootProps,
|
|
17
|
+
type ChipRootProps as RootProps,
|
|
18
|
+
type ChipSuffixIconProps as SuffixIconProps,
|
|
19
|
+
type ChipToggleProps as ToggleProps,
|
|
20
|
+
} from "./Chip";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { fireEvent, render } from "@lynx-js/react/testing-library";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import * as Chip from "./Chip.namespace";
|
|
6
|
+
|
|
7
|
+
function getRenderedRoot() {
|
|
8
|
+
const root = elementTree.root;
|
|
9
|
+
|
|
10
|
+
if (!root) {
|
|
11
|
+
throw new Error("Expected Lynx render root to exist.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getChipRoot() {
|
|
18
|
+
const root = getRenderedRoot();
|
|
19
|
+
|
|
20
|
+
if (root.classList.contains("seed-chip__root")) return root;
|
|
21
|
+
|
|
22
|
+
const chipRoot = root.querySelector<HTMLElement>(".seed-chip__root");
|
|
23
|
+
if (!chipRoot) throw new Error("Expected Chip root to exist.");
|
|
24
|
+
|
|
25
|
+
return chipRoot;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("Chip", () => {
|
|
29
|
+
it("renders the button and label recipe slots", () => {
|
|
30
|
+
render(
|
|
31
|
+
<Chip.Button size="large" variant="outlineStrong">
|
|
32
|
+
<Chip.Label>버튼 칩</Chip.Label>
|
|
33
|
+
</Chip.Button>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const root = getChipRoot();
|
|
37
|
+
const label = root.querySelector(".seed-chip__label");
|
|
38
|
+
|
|
39
|
+
expect(root).toHaveClass("seed-chip__root--size_large");
|
|
40
|
+
expect(root).toHaveClass("seed-chip__root--variant_outlineStrong");
|
|
41
|
+
expect(label).toHaveTextContent("버튼 칩");
|
|
42
|
+
expect(label).toHaveClass("seed-chip__label--size_large");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("toggles uncontrolled selected state", () => {
|
|
46
|
+
const onCheckedChange = vi.fn();
|
|
47
|
+
render(
|
|
48
|
+
<Chip.Toggle onCheckedChange={onCheckedChange}>
|
|
49
|
+
<Chip.Label>토글 칩</Chip.Label>
|
|
50
|
+
</Chip.Toggle>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const root = getChipRoot();
|
|
54
|
+
expect(root).toHaveClass("seed-chip__root--selected_false");
|
|
55
|
+
|
|
56
|
+
fireEvent.tap(root);
|
|
57
|
+
|
|
58
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
59
|
+
expect(getChipRoot()).toHaveClass("seed-chip__root--selected_true");
|
|
60
|
+
expect(getChipRoot()).toHaveAttribute("accessibility-traits", "selected");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not toggle when disabled", () => {
|
|
64
|
+
const onCheckedChange = vi.fn();
|
|
65
|
+
render(
|
|
66
|
+
<Chip.Toggle disabled onCheckedChange={onCheckedChange}>
|
|
67
|
+
<Chip.Label>비활성 칩</Chip.Label>
|
|
68
|
+
</Chip.Toggle>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const root = getChipRoot();
|
|
72
|
+
fireEvent.tap(root);
|
|
73
|
+
|
|
74
|
+
expect(onCheckedChange).not.toHaveBeenCalled();
|
|
75
|
+
expect(root).toHaveClass("seed-chip__root--disabled_true");
|
|
76
|
+
expect(root).toHaveAttribute("accessibility-traits", "disabled");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("selects one radio item and reports the value", () => {
|
|
80
|
+
const onValueChange = vi.fn();
|
|
81
|
+
render(
|
|
82
|
+
<Chip.RadioRoot defaultValue="first" onValueChange={onValueChange}>
|
|
83
|
+
<Chip.RadioItem value="first">
|
|
84
|
+
<Chip.Label>첫 번째</Chip.Label>
|
|
85
|
+
</Chip.RadioItem>
|
|
86
|
+
<Chip.RadioItem value="second">
|
|
87
|
+
<Chip.Label>두 번째</Chip.Label>
|
|
88
|
+
</Chip.RadioItem>
|
|
89
|
+
</Chip.RadioRoot>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const roots = getRenderedRoot().querySelectorAll(".seed-chip__root");
|
|
93
|
+
expect(roots[0]).toHaveClass("seed-chip__root--selected_true");
|
|
94
|
+
expect(roots[1]).toHaveClass("seed-chip__root--selected_false");
|
|
95
|
+
|
|
96
|
+
fireEvent.tap(roots[1] as HTMLElement);
|
|
97
|
+
|
|
98
|
+
expect(onValueChange).toHaveBeenCalledWith("second");
|
|
99
|
+
const updatedRoots = getRenderedRoot().querySelectorAll(".seed-chip__root");
|
|
100
|
+
expect(updatedRoots[0]).toHaveClass("seed-chip__root--selected_false");
|
|
101
|
+
expect(updatedRoots[1]).toHaveClass("seed-chip__root--selected_true");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("requires radio items to be rendered inside a radio root", () => {
|
|
105
|
+
expect(() => {
|
|
106
|
+
render(
|
|
107
|
+
<Chip.RadioItem value="orphan">
|
|
108
|
+
<Chip.Label>고립된 칩</Chip.Label>
|
|
109
|
+
</Chip.RadioItem>,
|
|
110
|
+
);
|
|
111
|
+
}).toThrow(/must be rendered inside <ChipRadioRoot\/>/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import * as React from "@lynx-js/react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
import { chip, type ChipVariantProps } from "@seed-design/lynx-css/recipes/chip";
|
|
5
|
+
|
|
6
|
+
import { useControllableState } from "../../hooks/useControllableState";
|
|
7
|
+
import { usePressTap } from "../../hooks/usePressTap";
|
|
8
|
+
import type {
|
|
9
|
+
LynxAccessibilityProps,
|
|
10
|
+
LynxPressableProps,
|
|
11
|
+
LynxStyledElementProps,
|
|
12
|
+
LynxTextRef,
|
|
13
|
+
LynxViewRef,
|
|
14
|
+
} from "../../types";
|
|
15
|
+
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
16
|
+
import {
|
|
17
|
+
IconRequired,
|
|
18
|
+
IconSlotProvider,
|
|
19
|
+
PrefixIcon,
|
|
20
|
+
SuffixIcon,
|
|
21
|
+
type PrefixIconProps,
|
|
22
|
+
type SuffixIconProps,
|
|
23
|
+
} from "../Icon/Icon";
|
|
24
|
+
|
|
25
|
+
const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(chip);
|
|
26
|
+
|
|
27
|
+
type ChipPublicVariantProps = Omit<ChipVariantProps, "selected" | "pressed">;
|
|
28
|
+
|
|
29
|
+
interface ChipRootViewProps
|
|
30
|
+
extends ChipVariantProps,
|
|
31
|
+
LynxStyledElementProps,
|
|
32
|
+
LynxPressableProps,
|
|
33
|
+
LynxAccessibilityProps {}
|
|
34
|
+
|
|
35
|
+
const ChipRootView = React.forwardRef<unknown, ChipRootViewProps>((props, ref) => {
|
|
36
|
+
const [variantProps, otherProps] = chip.splitVariantProps(props);
|
|
37
|
+
const classes = chip(variantProps);
|
|
38
|
+
const { children, className, ...nativeProps } = otherProps;
|
|
39
|
+
const iconSlotContextValue = React.useMemo(
|
|
40
|
+
() => ({
|
|
41
|
+
classNames: {
|
|
42
|
+
icon: classes.icon,
|
|
43
|
+
prefixIcon: classes.prefixIcon,
|
|
44
|
+
suffixIcon: classes.suffixIcon,
|
|
45
|
+
},
|
|
46
|
+
deps: [
|
|
47
|
+
variantProps.variant,
|
|
48
|
+
variantProps.size,
|
|
49
|
+
variantProps.layout,
|
|
50
|
+
variantProps.selected,
|
|
51
|
+
variantProps.pressed,
|
|
52
|
+
variantProps.disabled,
|
|
53
|
+
],
|
|
54
|
+
}),
|
|
55
|
+
[
|
|
56
|
+
classes.icon,
|
|
57
|
+
classes.prefixIcon,
|
|
58
|
+
classes.suffixIcon,
|
|
59
|
+
variantProps.variant,
|
|
60
|
+
variantProps.size,
|
|
61
|
+
variantProps.layout,
|
|
62
|
+
variantProps.selected,
|
|
63
|
+
variantProps.pressed,
|
|
64
|
+
variantProps.disabled,
|
|
65
|
+
],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ClassNamesProvider value={classes}>
|
|
70
|
+
<IconSlotProvider value={iconSlotContextValue}>
|
|
71
|
+
<IconRequired enabled={variantProps.layout === "iconOnly"}>
|
|
72
|
+
<view
|
|
73
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
74
|
+
className={clsx(classes.root, className)}
|
|
75
|
+
{...nativeProps}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</view>
|
|
79
|
+
</IconRequired>
|
|
80
|
+
</IconSlotProvider>
|
|
81
|
+
</ClassNamesProvider>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
ChipRootView.displayName = "ChipRootView";
|
|
85
|
+
|
|
86
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @platform Lynx
|
|
90
|
+
*
|
|
91
|
+
* 웹 대비 미지원 기능:
|
|
92
|
+
* - HTML button 속성 및 native form 제출
|
|
93
|
+
* - 키보드 focus / focusVisible
|
|
94
|
+
* - `asChild`
|
|
95
|
+
*/
|
|
96
|
+
export interface ChipButtonProps
|
|
97
|
+
extends ChipPublicVariantProps,
|
|
98
|
+
LynxStyledElementProps,
|
|
99
|
+
LynxPressableProps,
|
|
100
|
+
LynxAccessibilityProps {
|
|
101
|
+
disabled?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const ChipButton = React.forwardRef<unknown, ChipButtonProps>((props, ref) => {
|
|
105
|
+
const {
|
|
106
|
+
bindtap,
|
|
107
|
+
"main-thread:bindtap": mainThreadBindtap,
|
|
108
|
+
disabled = false,
|
|
109
|
+
"accessibility-element": accessibilityElement = true,
|
|
110
|
+
"accessibility-traits": accessibilityTraits,
|
|
111
|
+
...restProps
|
|
112
|
+
} = props;
|
|
113
|
+
const { pressed, ...pressHandlers } = usePressTap({
|
|
114
|
+
disabled,
|
|
115
|
+
onTap: bindtap,
|
|
116
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<ChipRootView
|
|
121
|
+
ref={ref}
|
|
122
|
+
{...restProps}
|
|
123
|
+
disabled={disabled}
|
|
124
|
+
selected={false}
|
|
125
|
+
pressed={pressed}
|
|
126
|
+
accessibility-element={accessibilityElement}
|
|
127
|
+
accessibility-traits={disabled ? "disabled" : (accessibilityTraits ?? "button")}
|
|
128
|
+
{...pressHandlers}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
ChipButton.displayName = "ChipButton";
|
|
133
|
+
|
|
134
|
+
/** React `Chip.Root`에 대응하는 기본 action chip입니다. */
|
|
135
|
+
export interface ChipRootProps extends ChipButtonProps {}
|
|
136
|
+
|
|
137
|
+
export const ChipRoot = React.forwardRef<unknown, ChipRootProps>((props, ref) => (
|
|
138
|
+
<ChipButton ref={ref} {...props} />
|
|
139
|
+
));
|
|
140
|
+
ChipRoot.displayName = "ChipRoot";
|
|
141
|
+
|
|
142
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @platform Lynx
|
|
146
|
+
*
|
|
147
|
+
* 웹 대비 미지원 기능:
|
|
148
|
+
* - HiddenInput / inputProps / name / value: Lynx에 HTML form 제출 모델이 없음
|
|
149
|
+
* - raw DOM `onChange`: `onCheckedChange`로 대체
|
|
150
|
+
*/
|
|
151
|
+
export interface ChipToggleProps extends ChipButtonProps {
|
|
152
|
+
checked?: boolean;
|
|
153
|
+
defaultChecked?: boolean;
|
|
154
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const ChipToggle = React.forwardRef<unknown, ChipToggleProps>((props, ref) => {
|
|
158
|
+
const {
|
|
159
|
+
checked: checkedProp,
|
|
160
|
+
defaultChecked = false,
|
|
161
|
+
onCheckedChange,
|
|
162
|
+
bindtap,
|
|
163
|
+
"main-thread:bindtap": mainThreadBindtap,
|
|
164
|
+
disabled = false,
|
|
165
|
+
"accessibility-element": accessibilityElement = true,
|
|
166
|
+
"accessibility-traits": accessibilityTraits,
|
|
167
|
+
...restProps
|
|
168
|
+
} = props;
|
|
169
|
+
const [checked, setChecked] = useControllableState({
|
|
170
|
+
value: checkedProp,
|
|
171
|
+
defaultValue: defaultChecked,
|
|
172
|
+
onChange: onCheckedChange,
|
|
173
|
+
});
|
|
174
|
+
const handleTap = React.useCallback(
|
|
175
|
+
(...args: Parameters<NonNullable<LynxPressableProps["bindtap"]>>) => {
|
|
176
|
+
setChecked(!checked);
|
|
177
|
+
bindtap?.(...args);
|
|
178
|
+
},
|
|
179
|
+
[bindtap, checked, setChecked],
|
|
180
|
+
);
|
|
181
|
+
const { pressed, ...pressHandlers } = usePressTap({
|
|
182
|
+
disabled,
|
|
183
|
+
onTap: handleTap,
|
|
184
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<ChipRootView
|
|
189
|
+
ref={ref}
|
|
190
|
+
{...restProps}
|
|
191
|
+
disabled={disabled}
|
|
192
|
+
selected={checked}
|
|
193
|
+
pressed={pressed}
|
|
194
|
+
accessibility-element={accessibilityElement}
|
|
195
|
+
accessibility-traits={
|
|
196
|
+
disabled ? "disabled" : checked ? "selected" : (accessibilityTraits ?? "button")
|
|
197
|
+
}
|
|
198
|
+
{...pressHandlers}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
ChipToggle.displayName = "ChipToggle";
|
|
203
|
+
|
|
204
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
205
|
+
|
|
206
|
+
interface ChipRadioGroupContextValue {
|
|
207
|
+
value: string | null;
|
|
208
|
+
setValue: (value: string) => void;
|
|
209
|
+
disabled: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ChipRadioGroupContext = React.createContext<ChipRadioGroupContextValue | null>(null);
|
|
213
|
+
|
|
214
|
+
function useChipRadioGroupContext(consumer: string): ChipRadioGroupContextValue {
|
|
215
|
+
const context = React.useContext(ChipRadioGroupContext);
|
|
216
|
+
if (!context) {
|
|
217
|
+
throw new Error(`<${consumer}/> must be rendered inside <ChipRadioRoot/>.`);
|
|
218
|
+
}
|
|
219
|
+
return context;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface ChipRadioRootProps extends LynxStyledElementProps {
|
|
223
|
+
value?: string;
|
|
224
|
+
defaultValue?: string;
|
|
225
|
+
disabled?: boolean;
|
|
226
|
+
onValueChange?: (value: string) => void;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const ChipRadioRoot = React.forwardRef<unknown, ChipRadioRootProps>((props, ref) => {
|
|
230
|
+
const {
|
|
231
|
+
value: valueProp,
|
|
232
|
+
defaultValue,
|
|
233
|
+
disabled = false,
|
|
234
|
+
onValueChange,
|
|
235
|
+
children,
|
|
236
|
+
className,
|
|
237
|
+
...nativeProps
|
|
238
|
+
} = props;
|
|
239
|
+
const handleChange = React.useCallback(
|
|
240
|
+
(nextValue: string | null) => {
|
|
241
|
+
if (nextValue !== null) onValueChange?.(nextValue);
|
|
242
|
+
},
|
|
243
|
+
[onValueChange],
|
|
244
|
+
);
|
|
245
|
+
const [value, setValueInternal] = useControllableState<string | null>({
|
|
246
|
+
value: valueProp !== undefined ? valueProp : undefined,
|
|
247
|
+
defaultValue: defaultValue ?? null,
|
|
248
|
+
onChange: handleChange,
|
|
249
|
+
});
|
|
250
|
+
const setValue = React.useCallback(
|
|
251
|
+
(nextValue: string) => setValueInternal(nextValue),
|
|
252
|
+
[setValueInternal],
|
|
253
|
+
);
|
|
254
|
+
const contextValue = React.useMemo(
|
|
255
|
+
() => ({ value, setValue, disabled }),
|
|
256
|
+
[value, setValue, disabled],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<ChipRadioGroupContext.Provider value={contextValue}>
|
|
261
|
+
<view {...(ref ? { ref: ref as LynxViewRef } : {})} className={className} {...nativeProps}>
|
|
262
|
+
{children}
|
|
263
|
+
</view>
|
|
264
|
+
</ChipRadioGroupContext.Provider>
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
ChipRadioRoot.displayName = "ChipRadioRoot";
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @platform Lynx
|
|
271
|
+
*
|
|
272
|
+
* 웹 대비 미지원 기능:
|
|
273
|
+
* - HiddenInput / inputProps / name / required: Lynx에 HTML form 제출 모델이 없음
|
|
274
|
+
* - raw DOM `onChange`: `Chip.RadioRoot`의 `onValueChange`로 대체
|
|
275
|
+
*/
|
|
276
|
+
export interface ChipRadioItemProps extends ChipButtonProps {
|
|
277
|
+
value: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const ChipRadioItem = React.forwardRef<unknown, ChipRadioItemProps>((props, ref) => {
|
|
281
|
+
const {
|
|
282
|
+
value: itemValue,
|
|
283
|
+
disabled: itemDisabled = false,
|
|
284
|
+
bindtap,
|
|
285
|
+
"main-thread:bindtap": mainThreadBindtap,
|
|
286
|
+
"accessibility-element": accessibilityElement = true,
|
|
287
|
+
"accessibility-traits": accessibilityTraits,
|
|
288
|
+
...restProps
|
|
289
|
+
} = props;
|
|
290
|
+
const group = useChipRadioGroupContext("ChipRadioItem");
|
|
291
|
+
const disabled = group.disabled || itemDisabled;
|
|
292
|
+
const selected = group.value === itemValue;
|
|
293
|
+
const handleTap = React.useCallback(
|
|
294
|
+
(...args: Parameters<NonNullable<LynxPressableProps["bindtap"]>>) => {
|
|
295
|
+
if (!selected) group.setValue(itemValue);
|
|
296
|
+
bindtap?.(...args);
|
|
297
|
+
},
|
|
298
|
+
[bindtap, group, itemValue, selected],
|
|
299
|
+
);
|
|
300
|
+
const { pressed, ...pressHandlers } = usePressTap({
|
|
301
|
+
disabled,
|
|
302
|
+
onTap: handleTap,
|
|
303
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<ChipRootView
|
|
308
|
+
ref={ref}
|
|
309
|
+
{...restProps}
|
|
310
|
+
disabled={disabled}
|
|
311
|
+
selected={selected}
|
|
312
|
+
pressed={pressed}
|
|
313
|
+
accessibility-element={accessibilityElement}
|
|
314
|
+
accessibility-traits={
|
|
315
|
+
disabled ? "disabled" : selected ? "selected" : (accessibilityTraits ?? "button")
|
|
316
|
+
}
|
|
317
|
+
{...pressHandlers}
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
ChipRadioItem.displayName = "ChipRadioItem";
|
|
322
|
+
|
|
323
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
324
|
+
|
|
325
|
+
export interface ChipLabelProps extends LynxStyledElementProps {}
|
|
326
|
+
|
|
327
|
+
export const ChipLabel = React.forwardRef<unknown, ChipLabelProps>((props, ref) => {
|
|
328
|
+
const classes = useClassNames();
|
|
329
|
+
const { children, className, ...nativeProps } = props;
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<text
|
|
333
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
334
|
+
className={clsx(classes.label, className)}
|
|
335
|
+
{...nativeProps}
|
|
336
|
+
>
|
|
337
|
+
{children}
|
|
338
|
+
</text>
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
ChipLabel.displayName = "ChipLabel";
|
|
342
|
+
|
|
343
|
+
export interface ChipPrefixIconProps extends PrefixIconProps {}
|
|
344
|
+
|
|
345
|
+
export const ChipPrefixIcon = React.forwardRef<unknown, ChipPrefixIconProps>((props, ref) => (
|
|
346
|
+
<PrefixIcon ref={ref} {...props} />
|
|
347
|
+
));
|
|
348
|
+
ChipPrefixIcon.displayName = "ChipPrefixIcon";
|
|
349
|
+
|
|
350
|
+
export interface ChipPrefixAvatarProps extends LynxStyledElementProps {}
|
|
351
|
+
|
|
352
|
+
export const ChipPrefixAvatar = React.forwardRef<unknown, ChipPrefixAvatarProps>((props, ref) => {
|
|
353
|
+
const classes = useClassNames();
|
|
354
|
+
const { children, className, ...nativeProps } = props;
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<view
|
|
358
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
359
|
+
className={clsx(classes.prefixAvatar, className)}
|
|
360
|
+
{...nativeProps}
|
|
361
|
+
>
|
|
362
|
+
{children}
|
|
363
|
+
</view>
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
ChipPrefixAvatar.displayName = "ChipPrefixAvatar";
|
|
367
|
+
|
|
368
|
+
export interface ChipSuffixIconProps extends SuffixIconProps {}
|
|
369
|
+
|
|
370
|
+
export const ChipSuffixIcon = React.forwardRef<unknown, ChipSuffixIconProps>((props, ref) => (
|
|
371
|
+
<SuffixIcon ref={ref} {...props} />
|
|
372
|
+
));
|
|
373
|
+
ChipSuffixIcon.displayName = "ChipSuffixIcon";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ChipButton,
|
|
3
|
+
ChipLabel,
|
|
4
|
+
ChipPrefixAvatar,
|
|
5
|
+
ChipPrefixIcon,
|
|
6
|
+
ChipRadioItem,
|
|
7
|
+
ChipRadioRoot,
|
|
8
|
+
ChipRoot,
|
|
9
|
+
ChipSuffixIcon,
|
|
10
|
+
ChipToggle,
|
|
11
|
+
type ChipButtonProps,
|
|
12
|
+
type ChipLabelProps,
|
|
13
|
+
type ChipPrefixAvatarProps,
|
|
14
|
+
type ChipPrefixIconProps,
|
|
15
|
+
type ChipRadioItemProps,
|
|
16
|
+
type ChipRadioRootProps,
|
|
17
|
+
type ChipRootProps,
|
|
18
|
+
type ChipSuffixIconProps,
|
|
19
|
+
type ChipToggleProps,
|
|
20
|
+
} from "./Chip";
|
|
21
|
+
|
|
22
|
+
export * as Chip from "./Chip.namespace";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render } from "@lynx-js/react/testing-library";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Divider } from "./Divider";
|
|
6
|
+
|
|
7
|
+
function getRenderedDivider() {
|
|
8
|
+
const divider = elementTree.root?.firstElementChild;
|
|
9
|
+
|
|
10
|
+
if (!divider) {
|
|
11
|
+
throw new Error("Expected Divider to render a root element.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return divider as HTMLElement;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function expectStyle(style: CSSStyleDeclaration, expected: Record<string, string>) {
|
|
18
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
19
|
+
expect(style.getPropertyValue(key)).toBe(value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Divider", () => {
|
|
24
|
+
it("renders a horizontal semantic divider by default", () => {
|
|
25
|
+
render(<Divider className="custom-divider" />);
|
|
26
|
+
|
|
27
|
+
const divider = getRenderedDivider();
|
|
28
|
+
|
|
29
|
+
expect(divider).toHaveClass("custom-divider");
|
|
30
|
+
expect(divider).toHaveAttribute("accessibility-element", "true");
|
|
31
|
+
expect(divider).toHaveAttribute("accessibility-role-description", "separator");
|
|
32
|
+
expectStyle(divider.style, {
|
|
33
|
+
width: "100%",
|
|
34
|
+
"border-color": "var(--seed-color-stroke-neutral-muted)",
|
|
35
|
+
"border-bottom-width": "1px",
|
|
36
|
+
"border-right-width": "0px",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("supports vertical orientation, inset spacing, color, and thickness", () => {
|
|
41
|
+
render(
|
|
42
|
+
<Divider
|
|
43
|
+
orientation="vertical"
|
|
44
|
+
inset
|
|
45
|
+
color="palette.blue500"
|
|
46
|
+
thickness="2px"
|
|
47
|
+
accessibility-element={false}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const divider = getRenderedDivider();
|
|
52
|
+
|
|
53
|
+
expect(divider).toHaveAttribute("accessibility-element", "false");
|
|
54
|
+
expectStyle(divider.style, {
|
|
55
|
+
height: "calc(100% - 32px)",
|
|
56
|
+
"border-color": "var(--seed-color-palette-blue-500)",
|
|
57
|
+
"border-bottom-width": "0px",
|
|
58
|
+
"border-right-width": "2px",
|
|
59
|
+
"margin-top": "16px",
|
|
60
|
+
"margin-bottom": "16px",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("lets user style override the default layout style", () => {
|
|
65
|
+
render(<Divider style={{ width: "80%", marginLeft: "4px" }} inset />);
|
|
66
|
+
|
|
67
|
+
expectStyle(getRenderedDivider().style, {
|
|
68
|
+
width: "80%",
|
|
69
|
+
"margin-left": "4px",
|
|
70
|
+
"margin-right": "16px",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|