@seed-design/lynx-react 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Accordion/Accordion.d.ts +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.d.ts.map +1 -1
- package/lib/components/ProgressCircle/ProgressCircle.jsx +38 -7
- package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
- package/lib/components/Stack/Stack.d.ts.map +1 -1
- package/lib/components/Stack/Stack.jsx +11 -2
- 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 +406 -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/index.d.ts +3 -0
- package/lib/components/Tabs/index.d.ts.map +1 -0
- package/lib/components/TagGroup/TagGroup.d.ts +2 -1
- package/lib/components/TagGroup/TagGroup.d.ts.map +1 -1
- package/lib/components/TagGroup/TagGroup.jsx +4 -3
- package/lib/components/Text/Text.jsx +2 -2
- package/lib/components/TextField/TextField.d.ts +20 -2
- package/lib/components/TextField/TextField.d.ts.map +1 -1
- package/lib/components/TextField/TextField.jsx +25 -9
- 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 +3 -3
- 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/ProgressCircle/ProgressCircle.test.tsx +58 -0
- package/src/components/ProgressCircle/ProgressCircle.tsx +42 -5
- package/src/components/Stack/Stack.test.tsx +67 -0
- package/src/components/Stack/Stack.tsx +15 -1
- package/src/components/Tabs/Tabs.namespace.ts +16 -0
- package/src/components/Tabs/Tabs.test.tsx +181 -0
- package/src/components/Tabs/Tabs.tsx +706 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/TagGroup/TagGroup.test.tsx +73 -0
- package/src/components/TagGroup/TagGroup.tsx +4 -2
- package/src/components/TextField/TextField.test.tsx +163 -35
- package/src/components/TextField/TextField.tsx +61 -19
- 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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import type { ReactNode } from "@lynx-js/react";
|
|
3
|
+
import { render } from "@lynx-js/react/testing-library";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import * as TagGroup from "./TagGroup.namespace";
|
|
7
|
+
|
|
8
|
+
function getRenderedRoot() {
|
|
9
|
+
const root = elementTree.root;
|
|
10
|
+
|
|
11
|
+
if (!root) {
|
|
12
|
+
throw new Error("Expected Lynx render root to exist.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return root;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderTagGroup(separator?: ReactNode) {
|
|
19
|
+
render(
|
|
20
|
+
<TagGroup.Root separator={separator}>
|
|
21
|
+
<TagGroup.Item>
|
|
22
|
+
<TagGroup.ItemLabel>첫 번째</TagGroup.ItemLabel>
|
|
23
|
+
</TagGroup.Item>
|
|
24
|
+
<TagGroup.Item>
|
|
25
|
+
<TagGroup.ItemLabel>두 번째</TagGroup.ItemLabel>
|
|
26
|
+
</TagGroup.Item>
|
|
27
|
+
<TagGroup.Item>
|
|
28
|
+
<TagGroup.ItemLabel>세 번째</TagGroup.ItemLabel>
|
|
29
|
+
</TagGroup.Item>
|
|
30
|
+
</TagGroup.Root>,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("TagGroup", () => {
|
|
35
|
+
it("trims surrounding whitespace from the default string separator", () => {
|
|
36
|
+
renderTagGroup();
|
|
37
|
+
|
|
38
|
+
const separators = getRenderedRoot().querySelectorAll(".seed-tag-group__separator");
|
|
39
|
+
|
|
40
|
+
expect(separators).toHaveLength(2);
|
|
41
|
+
expect(Array.from(separators, (separator) => separator.textContent)).toEqual(["·", "·"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("trims surrounding whitespace from a custom string separator", () => {
|
|
45
|
+
renderTagGroup(" \n / \t ");
|
|
46
|
+
|
|
47
|
+
const separators = getRenderedRoot().querySelectorAll(".seed-tag-group__separator");
|
|
48
|
+
|
|
49
|
+
expect(Array.from(separators, (separator) => separator.textContent)).toEqual(["/", "/"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("preserves a non-string separator node", () => {
|
|
53
|
+
renderTagGroup(<text className="custom-separator"> 구분 </text>);
|
|
54
|
+
|
|
55
|
+
const customSeparators = getRenderedRoot().querySelectorAll(".custom-separator");
|
|
56
|
+
|
|
57
|
+
expect(customSeparators).toHaveLength(2);
|
|
58
|
+
expect(Array.from(customSeparators, (separator) => separator.textContent)).toEqual([
|
|
59
|
+
" 구분 ",
|
|
60
|
+
" 구분 ",
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps each separator and following item in the same wrapper", () => {
|
|
65
|
+
renderTagGroup();
|
|
66
|
+
|
|
67
|
+
const wrappers = getRenderedRoot().querySelectorAll(".seed-tag-group__separatorWrapper");
|
|
68
|
+
|
|
69
|
+
expect(wrappers).toHaveLength(2);
|
|
70
|
+
expect(wrappers[0]?.textContent).toBe("·두 번째");
|
|
71
|
+
expect(wrappers[1]?.textContent).toBe("·세 번째");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -31,7 +31,8 @@ export interface TagGroupRootProps
|
|
|
31
31
|
TagGroupItemVariantProps,
|
|
32
32
|
LynxStyledElementProps {
|
|
33
33
|
/**
|
|
34
|
-
* children 사이에 삽입되는 구분자. 기본값 `"
|
|
34
|
+
* children 사이에 삽입되는 구분자. 기본값 `"·"`.
|
|
35
|
+
* 문자열을 전달하면 앞뒤 공백을 제거합니다.
|
|
35
36
|
*/
|
|
36
37
|
separator?: React.ReactNode;
|
|
37
38
|
}
|
|
@@ -41,6 +42,7 @@ export const TagGroupRoot = React.forwardRef<unknown, TagGroupRootProps>((props,
|
|
|
41
42
|
splitMultipleVariantsProps(props, { tagGroup, tagGroupItem });
|
|
42
43
|
const { children, className, separator = " · ", ...nativeProps } = otherProps;
|
|
43
44
|
const classes = tagGroup(tagGroupVariantProps);
|
|
45
|
+
const normalizedSeparator = typeof separator === "string" ? separator.trim() : separator;
|
|
44
46
|
|
|
45
47
|
const visibleChildren = toArray(children);
|
|
46
48
|
|
|
@@ -61,7 +63,7 @@ export const TagGroupRoot = React.forwardRef<unknown, TagGroupRootProps>((props,
|
|
|
61
63
|
key={(child as React.ReactElement).key ?? index}
|
|
62
64
|
className={classes.separatorWrapper}
|
|
63
65
|
>
|
|
64
|
-
<text className={classes.separator}>{
|
|
66
|
+
<text className={classes.separator}>{normalizedSeparator}</text>
|
|
65
67
|
{child}
|
|
66
68
|
</view>
|
|
67
69
|
);
|
|
@@ -63,11 +63,17 @@ function getTextFieldRoot() {
|
|
|
63
63
|
return textFieldRoot;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function fireNativeEvent(
|
|
66
|
+
function fireNativeEvent(
|
|
67
|
+
nativeRef: NodesRef,
|
|
68
|
+
element: Element,
|
|
69
|
+
eventName: string,
|
|
70
|
+
detail: object,
|
|
71
|
+
bubbles = true,
|
|
72
|
+
) {
|
|
67
73
|
const EventConstructor = element.ownerDocument.defaultView?.CustomEvent;
|
|
68
74
|
if (!EventConstructor) throw new Error("Expected CustomEvent constructor to exist.");
|
|
69
75
|
|
|
70
|
-
const event = new EventConstructor(`bindEvent:${eventName}`, { bubbles
|
|
76
|
+
const event = new EventConstructor(`bindEvent:${eventName}`, { bubbles, detail });
|
|
71
77
|
Object.assign(event, { eventType: "bindEvent", eventName });
|
|
72
78
|
fireEvent(nativeRef as unknown as Element, event);
|
|
73
79
|
}
|
|
@@ -171,7 +177,9 @@ describe("TextField", () => {
|
|
|
171
177
|
expect(input).toHaveAttribute("name", "title");
|
|
172
178
|
expect(input).toHaveAttribute("placeholder", "제목");
|
|
173
179
|
expect(input).toHaveAttribute("default-value", "초기값");
|
|
174
|
-
expect(input).
|
|
180
|
+
expect(input).toHaveAttribute("show-soft-input-on-focus", "true");
|
|
181
|
+
expect(input).toHaveAttribute("android-set-soft-input-mode", "unspecified");
|
|
182
|
+
expect(input).not.toHaveAttribute("maxlength");
|
|
175
183
|
|
|
176
184
|
rerender(renderTextField());
|
|
177
185
|
|
|
@@ -196,6 +204,56 @@ describe("TextField", () => {
|
|
|
196
204
|
expect(input).toHaveAttribute("default-value", "");
|
|
197
205
|
});
|
|
198
206
|
|
|
207
|
+
it("replaces undefined soft keyboard props with safe native defaults", () => {
|
|
208
|
+
const { rerender } = render(
|
|
209
|
+
<TextField.Root>
|
|
210
|
+
<TextField.Input
|
|
211
|
+
show-soft-input-on-focus={undefined}
|
|
212
|
+
android-set-soft-input-mode={undefined}
|
|
213
|
+
/>
|
|
214
|
+
</TextField.Root>,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const input = getRenderedRoot().querySelector("input");
|
|
218
|
+
expect(input).toHaveAttribute("show-soft-input-on-focus", "true");
|
|
219
|
+
expect(input).toHaveAttribute("android-set-soft-input-mode", "unspecified");
|
|
220
|
+
|
|
221
|
+
rerender(
|
|
222
|
+
<TextField.Root>
|
|
223
|
+
<TextField.Textarea
|
|
224
|
+
show-soft-input-on-focus={undefined}
|
|
225
|
+
android-set-soft-input-mode={undefined}
|
|
226
|
+
/>
|
|
227
|
+
</TextField.Root>,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const textarea = getRenderedRoot().querySelector("textarea");
|
|
231
|
+
expect(textarea).toHaveAttribute("show-soft-input-on-focus", "true");
|
|
232
|
+
expect(textarea).toHaveAttribute("android-set-soft-input-mode", "unspecified");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("preserves explicit soft keyboard overrides", () => {
|
|
236
|
+
const { rerender } = render(
|
|
237
|
+
<TextField.Root>
|
|
238
|
+
<TextField.Input show-soft-input-on-focus={false} android-set-soft-input-mode="nothing" />
|
|
239
|
+
</TextField.Root>,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const input = getRenderedRoot().querySelector("input");
|
|
243
|
+
expect(input).toHaveAttribute("show-soft-input-on-focus", "false");
|
|
244
|
+
expect(input).toHaveAttribute("android-set-soft-input-mode", "nothing");
|
|
245
|
+
|
|
246
|
+
rerender(
|
|
247
|
+
<TextField.Root>
|
|
248
|
+
<TextField.Textarea show-soft-input-on-focus={false} android-set-soft-input-mode="resize" />
|
|
249
|
+
</TextField.Root>,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const textarea = getRenderedRoot().querySelector("textarea");
|
|
253
|
+
expect(textarea).toHaveAttribute("show-soft-input-on-focus", "false");
|
|
254
|
+
expect(textarea).toHaveAttribute("android-set-soft-input-mode", "resize");
|
|
255
|
+
});
|
|
256
|
+
|
|
199
257
|
it("initializes a non-empty value without invoking a native UI method", () => {
|
|
200
258
|
const invoke = vi.fn(() => ({ exec: vi.fn() }));
|
|
201
259
|
const setRef = (node: NodesRef | null) => {
|
|
@@ -543,8 +601,9 @@ describe("TextField", () => {
|
|
|
543
601
|
);
|
|
544
602
|
|
|
545
603
|
if (!inputRef.current) throw new Error("Expected native input ref to exist.");
|
|
546
|
-
expect(getRenderedRoot().querySelector("input")).
|
|
604
|
+
expect(getRenderedRoot().querySelector("input")).toHaveAttribute(
|
|
547
605
|
"android-set-soft-input-mode",
|
|
606
|
+
"unspecified",
|
|
548
607
|
);
|
|
549
608
|
|
|
550
609
|
// ReactLynx Testing Library runtime accepts NodesRef, but its public fireEvent type
|
|
@@ -627,8 +686,11 @@ describe("TextField", () => {
|
|
|
627
686
|
expect(getRenderedQueries().queryByText("secret")).toBeNull();
|
|
628
687
|
});
|
|
629
688
|
|
|
630
|
-
it("uses native
|
|
689
|
+
it("uses direct native autoresize on Android", () => {
|
|
690
|
+
setSystemInfo({ platform: "Android" });
|
|
691
|
+
|
|
631
692
|
const textareaRef = createRef<NodesRef>();
|
|
693
|
+
const bindlayoutchange = vi.fn();
|
|
632
694
|
const actions = {
|
|
633
695
|
focus: vi.fn(),
|
|
634
696
|
blur: vi.fn(),
|
|
@@ -642,6 +704,7 @@ describe("TextField", () => {
|
|
|
642
704
|
ref={textareaRef}
|
|
643
705
|
placeholder="내용"
|
|
644
706
|
android-set-soft-input-mode="nothing"
|
|
707
|
+
bindlayoutchange={bindlayoutchange}
|
|
645
708
|
/>
|
|
646
709
|
</TextField.Root>
|
|
647
710
|
</KeyboardAvoidanceActionsContext.Provider>,
|
|
@@ -649,37 +712,27 @@ describe("TextField", () => {
|
|
|
649
712
|
|
|
650
713
|
const root = getRenderedRoot();
|
|
651
714
|
const textarea = root.querySelector("textarea");
|
|
652
|
-
const textareaRoot = root.querySelector(".seed-text-input__textareaRoot");
|
|
653
715
|
if (!textarea) throw new Error("Expected native textarea to exist.");
|
|
654
|
-
if (!textareaRoot) throw new Error("Expected textarea sizing wrapper to exist.");
|
|
655
716
|
if (!textareaRef.current) throw new Error("Expected native textarea ref to exist.");
|
|
656
717
|
|
|
657
|
-
expect(
|
|
658
|
-
expect(textarea).toHaveClass("seed-text-
|
|
659
|
-
expect(textarea).not.toHaveClass("seed-text-input__textareaAndroidAutoresize--size_large");
|
|
718
|
+
expect(textarea).not.toHaveClass("seed-text-input__textareaNativeAutoresize");
|
|
719
|
+
expect(textarea).toHaveClass("seed-text-input__textareaAndroidAutoresize--size_large");
|
|
660
720
|
expect(textarea).toHaveClass("seed-text-input__textareaValue");
|
|
661
721
|
expect(textarea).not.toHaveClass("seed-text-input__textareaFixed");
|
|
662
722
|
expect(textarea).toHaveAttribute("bounces", "false");
|
|
723
|
+
expect(textarea).toHaveAttribute("show-soft-input-on-focus", "true");
|
|
663
724
|
expect(textarea).toHaveAttribute("default-value", "첫 줄\n둘째 줄\n");
|
|
664
725
|
expect(textarea).toHaveAttribute("android-fullscreen-mode", "false");
|
|
665
726
|
expect(textarea).toHaveAttribute("android-set-soft-input-mode", "nothing");
|
|
666
|
-
expect(
|
|
727
|
+
expect(textarea).not.toHaveAttribute("maxlength");
|
|
728
|
+
expect(root.querySelector(".seed-text-input__textareaRoot")).toBeNull();
|
|
667
729
|
|
|
668
|
-
fireNativeEvent(
|
|
730
|
+
fireNativeEvent(textareaRef.current, textarea, "layoutchange", {
|
|
669
731
|
width: 200,
|
|
670
732
|
height: 120,
|
|
671
733
|
});
|
|
672
734
|
expect(actions.layoutChanged).toHaveBeenCalledOnce();
|
|
673
|
-
|
|
674
|
-
const exec = vi.fn();
|
|
675
|
-
const invoke = vi.fn(() => ({ exec }));
|
|
676
|
-
textareaRef.current.invoke = invoke as unknown as NodesRef["invoke"];
|
|
677
|
-
fireEvent.tap(textareaRef.current as unknown as Element);
|
|
678
|
-
expect(invoke).not.toHaveBeenCalled();
|
|
679
|
-
|
|
680
|
-
fireEvent.tap(textareaRoot);
|
|
681
|
-
expect(invoke).toHaveBeenCalledWith(expect.objectContaining({ method: "focus" }));
|
|
682
|
-
expect(exec).toHaveBeenCalledOnce();
|
|
735
|
+
expect(bindlayoutchange).toHaveBeenCalledOnce();
|
|
683
736
|
});
|
|
684
737
|
|
|
685
738
|
it("uses native intrinsic autoresize with the Android-only line-spacing correction", () => {
|
|
@@ -695,7 +748,7 @@ describe("TextField", () => {
|
|
|
695
748
|
const textarea = root.querySelector("textarea");
|
|
696
749
|
if (!textarea) throw new Error("Expected native textarea to exist.");
|
|
697
750
|
|
|
698
|
-
expect(textarea).toHaveClass("seed-text-input__textareaNativeAutoresize");
|
|
751
|
+
expect(textarea).not.toHaveClass("seed-text-input__textareaNativeAutoresize");
|
|
699
752
|
expect(textarea).toHaveClass("seed-text-input__textareaAndroidAutoresize--size_large");
|
|
700
753
|
expect(textarea).toHaveAttribute("line-spacing", "3.2px");
|
|
701
754
|
expect(root.querySelector(".seed-text-input__textareaAutoresizeRoot--size_large")).toBeNull();
|
|
@@ -743,29 +796,98 @@ describe("TextField", () => {
|
|
|
743
796
|
expect(uncorrectedTextarea).toHaveAttribute("line-spacing", "0");
|
|
744
797
|
});
|
|
745
798
|
|
|
746
|
-
it("
|
|
799
|
+
it("uses an iOS-only sizing wrapper without intercepting native textarea taps", () => {
|
|
747
800
|
setSystemInfo({ platform: "iOS" });
|
|
748
801
|
|
|
802
|
+
const textareaRef = createRef<NodesRef>();
|
|
803
|
+
const bindlayoutchange = vi.fn();
|
|
804
|
+
const actions = {
|
|
805
|
+
focus: vi.fn(),
|
|
806
|
+
blur: vi.fn(),
|
|
807
|
+
layoutChanged: vi.fn(),
|
|
808
|
+
unregister: vi.fn(),
|
|
809
|
+
};
|
|
749
810
|
render(
|
|
750
|
-
<
|
|
751
|
-
<TextField.
|
|
752
|
-
|
|
811
|
+
<KeyboardAvoidanceActionsContext.Provider value={actions}>
|
|
812
|
+
<TextField.Root defaultValue={"첫 줄\n"}>
|
|
813
|
+
<TextField.Textarea ref={textareaRef} bindlayoutchange={bindlayoutchange} />
|
|
814
|
+
</TextField.Root>
|
|
815
|
+
</KeyboardAvoidanceActionsContext.Provider>,
|
|
753
816
|
);
|
|
754
817
|
|
|
755
818
|
const root = getRenderedRoot();
|
|
756
819
|
const textarea = root.querySelector("textarea");
|
|
757
|
-
|
|
820
|
+
const textareaRoot = root.querySelector(".seed-text-input__textareaRoot");
|
|
821
|
+
if (!textarea || !textareaRoot || !textareaRef.current) {
|
|
822
|
+
throw new Error("Expected native textarea and its iOS sizing wrapper to exist.");
|
|
823
|
+
}
|
|
758
824
|
|
|
759
825
|
expect(textarea).toHaveClass("seed-text-input__textareaNativeAutoresize");
|
|
760
826
|
expect(textarea).not.toHaveClass("seed-text-input__textareaAndroidAutoresize--size_large");
|
|
761
827
|
expect(textarea).not.toHaveAttribute("line-spacing");
|
|
762
|
-
expect(
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
828
|
+
expect(textareaRoot).toHaveClass("seed-text-input__textareaAutoresizeRoot--size_large");
|
|
829
|
+
expect(textareaRoot).toHaveAttribute("ignore-focus", "true");
|
|
830
|
+
|
|
831
|
+
const exec = vi.fn();
|
|
832
|
+
const invoke = vi.fn(() => ({ exec }));
|
|
833
|
+
textareaRef.current.invoke = invoke as unknown as NodesRef["invoke"];
|
|
834
|
+
|
|
835
|
+
fireEvent.tap(textareaRef.current as unknown as Element);
|
|
836
|
+
expect(invoke).not.toHaveBeenCalled();
|
|
837
|
+
|
|
838
|
+
fireEvent.tap(textareaRoot);
|
|
839
|
+
expect(invoke).toHaveBeenCalledWith(expect.objectContaining({ method: "focus" }));
|
|
840
|
+
expect(exec).toHaveBeenCalledOnce();
|
|
841
|
+
|
|
842
|
+
actions.layoutChanged.mockClear();
|
|
843
|
+
fireNativeEvent(
|
|
844
|
+
textareaRef.current,
|
|
845
|
+
textarea,
|
|
846
|
+
"layoutchange",
|
|
847
|
+
{
|
|
848
|
+
width: 200,
|
|
849
|
+
height: 80,
|
|
850
|
+
},
|
|
851
|
+
false,
|
|
852
|
+
);
|
|
853
|
+
expect(bindlayoutchange).toHaveBeenCalledOnce();
|
|
854
|
+
expect(actions.layoutChanged).not.toHaveBeenCalled();
|
|
855
|
+
|
|
856
|
+
fireNativeEvent(textareaRoot as unknown as NodesRef, textareaRoot, "layoutchange", {
|
|
857
|
+
width: 200,
|
|
858
|
+
height: 94,
|
|
859
|
+
});
|
|
860
|
+
expect(actions.layoutChanged).toHaveBeenCalledOnce();
|
|
766
861
|
});
|
|
767
862
|
|
|
768
|
-
it("does not
|
|
863
|
+
it("does not invoke focus from disabled iOS textarea wrapper padding", () => {
|
|
864
|
+
setSystemInfo({ platform: "iOS" });
|
|
865
|
+
|
|
866
|
+
const textareaRef = createRef<NodesRef>();
|
|
867
|
+
render(
|
|
868
|
+
<TextField.Root disabled>
|
|
869
|
+
<TextField.Textarea ref={textareaRef} />
|
|
870
|
+
</TextField.Root>,
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
const textareaRoot = getRenderedRoot().querySelector(".seed-text-input__textareaRoot");
|
|
874
|
+
if (!textareaRoot || !textareaRef.current) {
|
|
875
|
+
throw new Error("Expected disabled native textarea and its iOS sizing wrapper to exist.");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const invoke = vi.fn(() => ({ exec: vi.fn() }));
|
|
879
|
+
textareaRef.current.invoke = invoke as unknown as NodesRef["invoke"];
|
|
880
|
+
|
|
881
|
+
fireEvent.tap(textareaRoot);
|
|
882
|
+
expect(invoke).not.toHaveBeenCalled();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it.each([
|
|
886
|
+
"Android",
|
|
887
|
+
"iOS",
|
|
888
|
+
] as const)("does not restore the previous value after an accepted controlled textarea newline on %s", async (platform) => {
|
|
889
|
+
setSystemInfo({ platform });
|
|
890
|
+
|
|
769
891
|
const textareaRef = createRef<NodesRef>();
|
|
770
892
|
const onValueChange = vi.fn();
|
|
771
893
|
const bindinput = vi.fn();
|
|
@@ -794,8 +916,13 @@ describe("TextField", () => {
|
|
|
794
916
|
throw new Error("Expected native textarea to exist.");
|
|
795
917
|
}
|
|
796
918
|
|
|
797
|
-
|
|
798
|
-
|
|
919
|
+
if (platform === "iOS") {
|
|
920
|
+
expect(textarea).toHaveClass("seed-text-input__textareaNativeAutoresize");
|
|
921
|
+
expect(root.querySelector(".seed-text-input__textareaRoot")).not.toBeNull();
|
|
922
|
+
} else {
|
|
923
|
+
expect(textarea).not.toHaveClass("seed-text-input__textareaNativeAutoresize");
|
|
924
|
+
expect(root.querySelector(".seed-text-input__textareaRoot")).toBeNull();
|
|
925
|
+
}
|
|
799
926
|
|
|
800
927
|
const invoke = vi.fn(() => ({ exec: vi.fn() }));
|
|
801
928
|
textareaRef.current.invoke = invoke as unknown as NodesRef["invoke"];
|
|
@@ -811,6 +938,7 @@ describe("TextField", () => {
|
|
|
811
938
|
expect(invoke).not.toHaveBeenCalled();
|
|
812
939
|
|
|
813
940
|
await flushControlledReconciliation();
|
|
941
|
+
expect(getRenderedRoot().querySelector("textarea")).toBe(textarea);
|
|
814
942
|
expect(invoke).not.toHaveBeenCalled();
|
|
815
943
|
});
|
|
816
944
|
|
|
@@ -17,13 +17,21 @@ declare const SystemInfo: LynxSystemInfo | undefined;
|
|
|
17
17
|
|
|
18
18
|
const ANDROID_TEXTAREA_DEFAULT_LINE_SPACING = "3.2px" as const;
|
|
19
19
|
|
|
20
|
-
function
|
|
20
|
+
function getRuntimePlatform(): string | undefined {
|
|
21
21
|
const globalSystemInfo = (globalThis as typeof globalThis & { SystemInfo?: LynxSystemInfo })
|
|
22
22
|
.SystemInfo;
|
|
23
23
|
const systemInfo =
|
|
24
24
|
globalSystemInfo ?? (typeof SystemInfo === "undefined" ? undefined : SystemInfo);
|
|
25
25
|
|
|
26
|
-
return systemInfo?.platform
|
|
26
|
+
return systemInfo?.platform;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isAndroidRuntime(): boolean {
|
|
30
|
+
return getRuntimePlatform() === "Android";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isIOSRuntime(): boolean {
|
|
34
|
+
return getRuntimePlatform() === "iOS";
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(textInput);
|
|
@@ -562,13 +570,22 @@ export interface TextFieldInputProps extends NativeTextControlProps {
|
|
|
562
570
|
maxlength?: NativeInputProps["maxlength"];
|
|
563
571
|
readonly?: NativeInputProps["readonly"];
|
|
564
572
|
disabled?: NativeInputProps["disabled"];
|
|
573
|
+
/**
|
|
574
|
+
* 포커스할 때 시스템 키보드를 표시한다.
|
|
575
|
+
* `undefined`가 native attribute로 전달되지 않도록 `true`를 명시적으로 적용한다.
|
|
576
|
+
* @defaultValue true
|
|
577
|
+
*/
|
|
565
578
|
"show-soft-input-on-focus"?: NativeInputProps["show-soft-input-on-focus"];
|
|
566
579
|
"input-filter"?: NativeInputProps["input-filter"];
|
|
567
580
|
type?: NativeInputProps["type"];
|
|
568
581
|
"ios-auto-correct"?: NativeInputProps["ios-auto-correct"];
|
|
569
582
|
"ios-spell-check"?: NativeInputProps["ios-spell-check"];
|
|
570
583
|
"android-fullscreen-mode"?: NativeInputProps["android-fullscreen-mode"];
|
|
571
|
-
/**
|
|
584
|
+
/**
|
|
585
|
+
* Android host window의 soft input mode를 지정한다.
|
|
586
|
+
* `undefined`가 native attribute로 전달되지 않도록 `"unspecified"`를 명시적으로 적용한다.
|
|
587
|
+
* @defaultValue "unspecified"
|
|
588
|
+
*/
|
|
572
589
|
"android-set-soft-input-mode"?: AndroidSetSoftInputMode;
|
|
573
590
|
bindfocus?: NativeInputProps["bindfocus"];
|
|
574
591
|
bindblur?: NativeInputProps["bindblur"];
|
|
@@ -651,10 +668,10 @@ export const TextFieldInput = React.forwardRef<NodesRef, TextFieldInputProps>((p
|
|
|
651
668
|
className={clsx(classes.value, className)}
|
|
652
669
|
disabled={control.disabled}
|
|
653
670
|
readonly={control.readOnly}
|
|
654
|
-
show-soft-input-on-focus={
|
|
655
|
-
android-set-soft-input-mode={androidSetSoftInputMode}
|
|
671
|
+
show-soft-input-on-focus={showSoftInputOnFocus ?? true}
|
|
672
|
+
android-set-soft-input-mode={androidSetSoftInputMode ?? "unspecified"}
|
|
656
673
|
name={name ?? control.context.name}
|
|
657
|
-
|
|
674
|
+
{...(resolvedMaxLength === undefined ? {} : { maxlength: resolvedMaxLength })}
|
|
658
675
|
bindinput={control.handleInput}
|
|
659
676
|
bindselection={handleSelection}
|
|
660
677
|
bindfocus={control.handleFocus}
|
|
@@ -686,6 +703,11 @@ export interface TextFieldTextareaProps extends NativeTextControlProps {
|
|
|
686
703
|
"line-spacing"?: NativeTextareaProps["line-spacing"];
|
|
687
704
|
readonly?: NativeTextareaProps["readonly"];
|
|
688
705
|
disabled?: NativeTextareaProps["disabled"];
|
|
706
|
+
/**
|
|
707
|
+
* 포커스할 때 시스템 키보드를 표시한다.
|
|
708
|
+
* `undefined`가 native attribute로 전달되지 않도록 `true`를 명시적으로 적용한다.
|
|
709
|
+
* @defaultValue true
|
|
710
|
+
*/
|
|
689
711
|
"show-soft-input-on-focus"?: NativeTextareaProps["show-soft-input-on-focus"];
|
|
690
712
|
"input-filter"?: NativeTextareaProps["input-filter"];
|
|
691
713
|
"enable-scroll-bar"?: NativeTextareaProps["enable-scroll-bar"];
|
|
@@ -694,7 +716,11 @@ export interface TextFieldTextareaProps extends NativeTextControlProps {
|
|
|
694
716
|
"ios-spell-check"?: NativeTextareaProps["ios-spell-check"];
|
|
695
717
|
/** Android의 fullscreen extract input을 활성화한다. @defaultValue false */
|
|
696
718
|
"android-fullscreen-mode"?: NativeTextareaProps["android-fullscreen-mode"];
|
|
697
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* Android host window의 soft input mode를 지정한다.
|
|
721
|
+
* `undefined`가 native attribute로 전달되지 않도록 `"unspecified"`를 명시적으로 적용한다.
|
|
722
|
+
* @defaultValue "unspecified"
|
|
723
|
+
*/
|
|
698
724
|
"android-set-soft-input-mode"?: AndroidSetSoftInputMode;
|
|
699
725
|
bindfocus?: NativeTextareaProps["bindfocus"];
|
|
700
726
|
bindblur?: NativeTextareaProps["bindblur"];
|
|
@@ -738,6 +764,7 @@ export const TextFieldTextarea = React.forwardRef<NodesRef, TextFieldTextareaPro
|
|
|
738
764
|
control.nativeInsertionMaxLength,
|
|
739
765
|
);
|
|
740
766
|
const isAndroid = isAndroidRuntime();
|
|
767
|
+
const usesIOSAutoresizeWrapper = autoresize && isIOSRuntime();
|
|
741
768
|
// Android native textarea는 CSS line-height를 무시한다. 실기기에서 관측한
|
|
742
769
|
// native font metrics와 SEED line box의 차이를 line-spacing으로 보정한다.
|
|
743
770
|
// 명시적인 값(0 포함)은 내부 기본값보다 우선한다.
|
|
@@ -756,16 +783,29 @@ export const TextFieldTextarea = React.forwardRef<NodesRef, TextFieldTextareaPro
|
|
|
756
783
|
},
|
|
757
784
|
[bindselection, control.handleSelectionChange],
|
|
758
785
|
);
|
|
786
|
+
const handleLayoutChange = React.useCallback<
|
|
787
|
+
NonNullable<NativeTextareaProps["bindlayoutchange"]>
|
|
788
|
+
>(
|
|
789
|
+
(event) => {
|
|
790
|
+
"background only";
|
|
791
|
+
|
|
792
|
+
if (autoresize) {
|
|
793
|
+
control.notifyLayoutChanged();
|
|
794
|
+
}
|
|
795
|
+
bindlayoutchange?.(event);
|
|
796
|
+
},
|
|
797
|
+
[autoresize, bindlayoutchange, control.notifyLayoutChanged],
|
|
798
|
+
);
|
|
759
799
|
const handleTextareaRootTap = React.useCallback<
|
|
760
800
|
NonNullable<IntrinsicElements["view"]["bindtap"]>
|
|
761
801
|
>(
|
|
762
802
|
(event) => {
|
|
763
803
|
"background only";
|
|
764
804
|
|
|
765
|
-
if (event.target.uid !== event.currentTarget.uid) return;
|
|
805
|
+
if (control.disabled || event.target.uid !== event.currentTarget.uid) return;
|
|
766
806
|
control.focusNativeControl();
|
|
767
807
|
},
|
|
768
|
-
[control.focusNativeControl],
|
|
808
|
+
[control.disabled, control.focusNativeControl],
|
|
769
809
|
);
|
|
770
810
|
|
|
771
811
|
if (control.readOnly) {
|
|
@@ -795,8 +835,8 @@ export const TextFieldTextarea = React.forwardRef<NodesRef, TextFieldTextareaPro
|
|
|
795
835
|
);
|
|
796
836
|
}
|
|
797
837
|
|
|
798
|
-
//
|
|
799
|
-
//
|
|
838
|
+
// iOS textarea는 첫 편집에서 native contentSize를 높이에 반영한다.
|
|
839
|
+
// 디자인 padding을 wrapper로 분리해 이때 padding만큼 높이가 중복되지 않게 한다.
|
|
800
840
|
const textarea = (
|
|
801
841
|
<textarea
|
|
802
842
|
{...control.defaultValueProps}
|
|
@@ -804,33 +844,35 @@ export const TextFieldTextarea = React.forwardRef<NodesRef, TextFieldTextareaPro
|
|
|
804
844
|
className={clsx(
|
|
805
845
|
classes.value,
|
|
806
846
|
classes.textareaValue,
|
|
807
|
-
autoresize
|
|
808
|
-
|
|
847
|
+
!autoresize && classes.textareaFixed,
|
|
848
|
+
usesIOSAutoresizeWrapper && classes.textareaNativeAutoresize,
|
|
849
|
+
autoresize && !usesIOSAutoresizeWrapper && classes.textareaAndroidAutoresize,
|
|
809
850
|
className,
|
|
810
851
|
)}
|
|
811
852
|
disabled={control.disabled}
|
|
812
853
|
readonly={control.readOnly}
|
|
813
|
-
show-soft-input-on-focus={
|
|
854
|
+
show-soft-input-on-focus={showSoftInputOnFocus ?? true}
|
|
814
855
|
bounces={bounces ?? (autoresize ? false : undefined)}
|
|
815
856
|
line-spacing={resolvedLineSpacing}
|
|
816
857
|
android-fullscreen-mode={androidFullscreenMode ?? false}
|
|
817
|
-
android-set-soft-input-mode={androidSetSoftInputMode}
|
|
858
|
+
android-set-soft-input-mode={androidSetSoftInputMode ?? "unspecified"}
|
|
818
859
|
name={name ?? control.context.name}
|
|
819
|
-
|
|
860
|
+
{...(resolvedMaxLength === undefined ? {} : { maxlength: resolvedMaxLength })}
|
|
820
861
|
bindinput={control.handleInput}
|
|
821
862
|
bindselection={handleSelection}
|
|
822
863
|
bindfocus={control.handleFocus}
|
|
823
864
|
bindblur={control.handleBlur}
|
|
824
|
-
bindlayoutchange={bindlayoutchange}
|
|
865
|
+
bindlayoutchange={usesIOSAutoresizeWrapper ? bindlayoutchange : handleLayoutChange}
|
|
825
866
|
{...nativeProps}
|
|
826
867
|
/>
|
|
827
868
|
);
|
|
828
869
|
|
|
829
|
-
if (!
|
|
870
|
+
if (!usesIOSAutoresizeWrapper) return textarea;
|
|
830
871
|
|
|
831
872
|
return (
|
|
832
873
|
<view
|
|
833
|
-
|
|
874
|
+
ignore-focus={true}
|
|
875
|
+
className={clsx(classes.textareaRoot, classes.textareaAutoresizeRoot)}
|
|
834
876
|
bindtap={handleTextareaRootTap}
|
|
835
877
|
bindlayoutchange={control.notifyLayoutChanged}
|
|
836
878
|
>
|
package/src/components/index.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
export * from "./Accordion";
|
|
1
2
|
export * from "./ActionButton";
|
|
2
3
|
export * from "./AppBar";
|
|
3
4
|
export * from "./Badge";
|
|
4
5
|
export * from "./Box";
|
|
5
6
|
export * from "./BottomSheet";
|
|
7
|
+
export * from "./Callout";
|
|
6
8
|
export * from "./Checkbox";
|
|
9
|
+
export * from "./Chip";
|
|
10
|
+
export * from "./Divider";
|
|
7
11
|
export * from "./Field";
|
|
8
12
|
export * from "./Icon";
|
|
9
13
|
export * from "./KeyboardAvoidingScrollView";
|
|
@@ -11,6 +15,7 @@ export * from "./ProgressCircle";
|
|
|
11
15
|
export * from "./RadioGroup";
|
|
12
16
|
export * from "./Stack";
|
|
13
17
|
export * from "./Switch";
|
|
18
|
+
export * from "./Tabs";
|
|
14
19
|
export * from "./TagGroup";
|
|
15
20
|
export * from "./Text";
|
|
16
21
|
export * from "./TextField";
|