@seed-design/lynx-react 0.3.1 → 0.4.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.
Files changed (78) hide show
  1. package/lib/components/Field/Field.d.ts +47 -0
  2. package/lib/components/Field/Field.d.ts.map +1 -0
  3. package/lib/components/Field/Field.jsx +121 -0
  4. package/lib/components/Field/Field.namespace.d.ts +2 -0
  5. package/lib/components/Field/Field.namespace.d.ts.map +1 -0
  6. package/lib/components/Field/Field.namespace.js +16 -0
  7. package/lib/components/Field/context.d.ts +19 -0
  8. package/lib/components/Field/context.d.ts.map +1 -0
  9. package/lib/components/Field/context.js +10 -0
  10. package/lib/components/Field/index.d.ts +3 -0
  11. package/lib/components/Field/index.d.ts.map +1 -0
  12. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts +42 -0
  13. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts.map +1 -0
  14. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.jsx +140 -0
  15. package/lib/components/KeyboardAvoidingScrollView/context.d.ts +17 -0
  16. package/lib/components/KeyboardAvoidingScrollView/context.d.ts.map +1 -0
  17. package/lib/components/KeyboardAvoidingScrollView/context.js +12 -0
  18. package/lib/components/KeyboardAvoidingScrollView/engine.d.ts +50 -0
  19. package/lib/components/KeyboardAvoidingScrollView/engine.d.ts.map +1 -0
  20. package/lib/components/KeyboardAvoidingScrollView/engine.js +259 -0
  21. package/lib/components/KeyboardAvoidingScrollView/geometry.d.ts +50 -0
  22. package/lib/components/KeyboardAvoidingScrollView/geometry.d.ts.map +1 -0
  23. package/lib/components/KeyboardAvoidingScrollView/geometry.js +90 -0
  24. package/lib/components/KeyboardAvoidingScrollView/index.d.ts +2 -0
  25. package/lib/components/KeyboardAvoidingScrollView/index.d.ts.map +1 -0
  26. package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.d.ts +11 -0
  27. package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.d.ts.map +1 -0
  28. package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.js +43 -0
  29. package/lib/components/KeyboardAvoidingScrollView/native-driver.d.ts +34 -0
  30. package/lib/components/KeyboardAvoidingScrollView/native-driver.d.ts.map +1 -0
  31. package/lib/components/KeyboardAvoidingScrollView/native-driver.js +133 -0
  32. package/lib/components/TextField/TextField.d.ts +113 -0
  33. package/lib/components/TextField/TextField.d.ts.map +1 -0
  34. package/lib/components/TextField/TextField.jsx +418 -0
  35. package/lib/components/TextField/TextField.namespace.d.ts +2 -0
  36. package/lib/components/TextField/TextField.namespace.d.ts.map +1 -0
  37. package/lib/components/TextField/TextField.namespace.js +14 -0
  38. package/lib/components/TextField/context.d.ts +22 -0
  39. package/lib/components/TextField/context.d.ts.map +1 -0
  40. package/lib/components/TextField/context.js +7 -0
  41. package/lib/components/TextField/index.d.ts +4 -0
  42. package/lib/components/TextField/index.d.ts.map +1 -0
  43. package/lib/components/TextField/useTextFieldWithGraphemes.d.ts +24 -0
  44. package/lib/components/TextField/useTextFieldWithGraphemes.d.ts.map +1 -0
  45. package/lib/components/TextField/useTextFieldWithGraphemes.js +47 -0
  46. package/lib/components/index.d.ts +3 -0
  47. package/lib/components/index.d.ts.map +1 -1
  48. package/lib/index.js +7 -1
  49. package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/_grapheme_data.js +8 -0
  50. package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/_incb_data.js +14 -0
  51. package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/core.js +68 -0
  52. package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/grapheme.js +175 -0
  53. package/package.json +6 -4
  54. package/src/components/Field/Field.namespace.ts +19 -0
  55. package/src/components/Field/Field.test.tsx +81 -0
  56. package/src/components/Field/Field.tsx +252 -0
  57. package/src/components/Field/context.ts +26 -0
  58. package/src/components/Field/index.ts +21 -0
  59. package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.test.tsx +267 -0
  60. package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.tsx +299 -0
  61. package/src/components/KeyboardAvoidingScrollView/context.ts +24 -0
  62. package/src/components/KeyboardAvoidingScrollView/engine.test.ts +451 -0
  63. package/src/components/KeyboardAvoidingScrollView/engine.ts +417 -0
  64. package/src/components/KeyboardAvoidingScrollView/geometry.test.ts +315 -0
  65. package/src/components/KeyboardAvoidingScrollView/geometry.ts +194 -0
  66. package/src/components/KeyboardAvoidingScrollView/index.ts +5 -0
  67. package/src/components/KeyboardAvoidingScrollView/keyboard-event-source.test.ts +122 -0
  68. package/src/components/KeyboardAvoidingScrollView/keyboard-event-source.ts +75 -0
  69. package/src/components/KeyboardAvoidingScrollView/native-driver.test.ts +223 -0
  70. package/src/components/KeyboardAvoidingScrollView/native-driver.ts +248 -0
  71. package/src/components/TextField/TextField.namespace.ts +16 -0
  72. package/src/components/TextField/TextField.test.tsx +924 -0
  73. package/src/components/TextField/TextField.tsx +912 -0
  74. package/src/components/TextField/context.ts +33 -0
  75. package/src/components/TextField/index.ts +23 -0
  76. package/src/components/TextField/useTextFieldWithGraphemes.test.tsx +80 -0
  77. package/src/components/TextField/useTextFieldWithGraphemes.ts +78 -0
  78. package/src/components/index.ts +3 -0
@@ -0,0 +1,33 @@
1
+ import * as React from "@lynx-js/react";
2
+ import type { NodesRef } from "@lynx-js/types";
3
+
4
+ /** iOS와 Android의 32비트 `maxlength` setter에서 사용하는 무제한 센티널. */
5
+ export const NATIVE_TEXT_MAX_LENGTH_UNLIMITED = 2_147_483_647;
6
+
7
+ export interface TextFieldContextValue {
8
+ rootRef: React.RefObject<NodesRef | null>;
9
+ value: string;
10
+ /** @internal controlled 입력의 native value 조정 여부를 판별한다. */
11
+ controlled: boolean;
12
+ nativeInsertionMaxLength?: number;
13
+ disabled: boolean;
14
+ invalid: boolean;
15
+ readOnly: boolean;
16
+ required: boolean;
17
+ name?: string;
18
+ focused: boolean;
19
+ setValue: (value: string) => void;
20
+ setFocused: (focused: boolean) => void;
21
+ }
22
+
23
+ export const TextFieldContext = React.createContext<TextFieldContextValue | null>(null);
24
+
25
+ export function useTextFieldContext(): TextFieldContextValue {
26
+ const context = React.useContext(TextFieldContext);
27
+
28
+ if (!context) {
29
+ throw new Error("TextField compound components must be rendered inside <TextField.Root>.");
30
+ }
31
+
32
+ return context;
33
+ }
@@ -0,0 +1,23 @@
1
+ export {
2
+ TextFieldPrefixIcon,
3
+ TextFieldPrefixText,
4
+ TextFieldRoot,
5
+ TextFieldSuffixIcon,
6
+ TextFieldSuffixText,
7
+ TextFieldInput,
8
+ TextFieldTextarea,
9
+ type TextFieldPrefixIconProps,
10
+ type TextFieldPrefixTextProps,
11
+ type TextFieldRootProps,
12
+ type TextFieldSuffixIconProps,
13
+ type TextFieldSuffixTextProps,
14
+ type TextFieldInputProps,
15
+ type TextFieldTextareaProps,
16
+ } from "./TextField";
17
+
18
+ export {
19
+ useTextFieldWithGraphemes,
20
+ type UseTextFieldWithGraphemesParams,
21
+ } from "./useTextFieldWithGraphemes";
22
+
23
+ export * as TextField from "./TextField.namespace";
@@ -0,0 +1,80 @@
1
+ import { act, renderHook } from "@lynx-js/react/testing-library";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { NATIVE_TEXT_MAX_LENGTH_UNLIMITED } from "./context";
5
+ import { useTextFieldWithGraphemes } from "./useTextFieldWithGraphemes";
6
+
7
+ describe("useTextFieldWithGraphemes", () => {
8
+ it("counts extended grapheme clusters", () => {
9
+ const { result } = renderHook(() =>
10
+ useTextFieldWithGraphemes({ defaultValue: "👨‍👩‍👧‍👦🏳️‍🌈é" }),
11
+ );
12
+
13
+ expect(result.current.graphemes).toEqual(["👨‍👩‍👧‍👦", "🏳️‍🌈", "é"]);
14
+ expect(result.current.counterProps).toEqual({ current: 3, max: 0 });
15
+ });
16
+
17
+ it("reports the original and grapheme-sliced values", () => {
18
+ const onValueChange = vi.fn();
19
+ const { result } = renderHook(() =>
20
+ useTextFieldWithGraphemes({ maxGraphemeCount: 2, onValueChange }),
21
+ );
22
+
23
+ act(() => {
24
+ result.current.textFieldRootProps.onValueChange("A👨‍👩‍👧‍👦B");
25
+ });
26
+
27
+ expect(onValueChange).toHaveBeenCalledWith({
28
+ value: "A👨‍👩‍👧‍👦B",
29
+ graphemes: ["A", "👨‍👩‍👧‍👦", "B"],
30
+ slicedValue: "A👨‍👩‍👧‍👦",
31
+ slicedGraphemes: ["A", "👨‍👩‍👧‍👦"],
32
+ });
33
+ expect(result.current.counterProps).toEqual({ current: 3, max: 2 });
34
+ });
35
+
36
+ it("keeps a controlled value fixed", () => {
37
+ const onValueChange = vi.fn();
38
+ const { result } = renderHook(() =>
39
+ useTextFieldWithGraphemes({ value: "고정", onValueChange }),
40
+ );
41
+
42
+ act(() => {
43
+ result.current.textFieldRootProps.onValueChange("변경");
44
+ });
45
+
46
+ expect(result.current.textFieldRootProps.value).toBe("고정");
47
+ expect(onValueChange).toHaveBeenCalledOnce();
48
+ });
49
+
50
+ it("leaves native insertion maxlength unmanaged without a grapheme limit", () => {
51
+ const { result } = renderHook(() => useTextFieldWithGraphemes({ defaultValue: "제한 없음" }));
52
+
53
+ expect(result.current.textFieldRootProps.nativeInsertionMaxLength).toBeUndefined();
54
+ });
55
+
56
+ it("provides a UTF-16 native insertion cap only at the grapheme limit", () => {
57
+ const family = "👨‍👩‍👧‍👦";
58
+ const { result, rerender } = renderHook(
59
+ ({ value }: { value: string }) => useTextFieldWithGraphemes({ value, maxGraphemeCount: 2 }),
60
+ { initialProps: { value: family } },
61
+ );
62
+
63
+ expect(result.current.textFieldRootProps.nativeInsertionMaxLength).toBe(
64
+ NATIVE_TEXT_MAX_LENGTH_UNLIMITED,
65
+ );
66
+
67
+ const valueAtLimit = `${family}A`;
68
+ rerender({ value: valueAtLimit });
69
+
70
+ expect(result.current.graphemes).toEqual([family, "A"]);
71
+ expect(result.current.textFieldRootProps.nativeInsertionMaxLength).toBe(valueAtLimit.length);
72
+ expect(result.current.textFieldRootProps.nativeInsertionMaxLength).toBeGreaterThan(2);
73
+
74
+ rerender({ value: "A" });
75
+
76
+ expect(result.current.textFieldRootProps.nativeInsertionMaxLength).toBe(
77
+ NATIVE_TEXT_MAX_LENGTH_UNLIMITED,
78
+ );
79
+ });
80
+ });
@@ -0,0 +1,78 @@
1
+ import * as React from "@lynx-js/react";
2
+ import { splitGraphemes } from "unicode-segmenter/grapheme";
3
+
4
+ import { NATIVE_TEXT_MAX_LENGTH_UNLIMITED } from "./context";
5
+
6
+ export interface UseTextFieldWithGraphemesParams {
7
+ maxGraphemeCount?: number;
8
+ value?: string;
9
+ defaultValue?: string;
10
+ onValueChange?: (values: {
11
+ value: string;
12
+ graphemes: string[];
13
+ slicedValue: string;
14
+ slicedGraphemes: string[];
15
+ }) => void;
16
+ }
17
+
18
+ let cachedValue = "";
19
+ let cachedGraphemes: string[] = [];
20
+
21
+ function getGraphemes(value: string): string[] {
22
+ if (value === cachedValue) return cachedGraphemes;
23
+
24
+ cachedValue = value;
25
+ cachedGraphemes = Array.from(splitGraphemes(value));
26
+ return cachedGraphemes;
27
+ }
28
+
29
+ export function useTextFieldWithGraphemes({
30
+ maxGraphemeCount,
31
+ value: controlledValue,
32
+ defaultValue = "",
33
+ onValueChange,
34
+ }: UseTextFieldWithGraphemesParams) {
35
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
36
+ const isControlled = controlledValue !== undefined;
37
+ const value = isControlled ? controlledValue : uncontrolledValue;
38
+ const graphemes = React.useMemo(() => getGraphemes(value), [value]);
39
+
40
+ const handleValueChange = React.useCallback(
41
+ (nextValue: string) => {
42
+ "background only";
43
+
44
+ const nextGraphemes = getGraphemes(nextValue);
45
+ const slicedGraphemes =
46
+ maxGraphemeCount === undefined ? nextGraphemes : nextGraphemes.slice(0, maxGraphemeCount);
47
+
48
+ if (!isControlled) {
49
+ setUncontrolledValue(nextValue);
50
+ }
51
+ onValueChange?.({
52
+ value: nextValue,
53
+ graphemes: nextGraphemes,
54
+ slicedValue: slicedGraphemes.join(""),
55
+ slicedGraphemes,
56
+ });
57
+ },
58
+ [isControlled, maxGraphemeCount, onValueChange],
59
+ );
60
+
61
+ return {
62
+ textFieldRootProps: {
63
+ value,
64
+ onValueChange: handleValueChange,
65
+ nativeInsertionMaxLength:
66
+ maxGraphemeCount === undefined
67
+ ? undefined
68
+ : graphemes.length >= maxGraphemeCount
69
+ ? value.length
70
+ : NATIVE_TEXT_MAX_LENGTH_UNLIMITED,
71
+ },
72
+ counterProps: {
73
+ current: graphemes.length,
74
+ max: maxGraphemeCount ?? 0,
75
+ },
76
+ graphemes,
77
+ };
78
+ }
@@ -4,10 +4,13 @@ export * from "./Badge";
4
4
  export * from "./Box";
5
5
  export * from "./BottomSheet";
6
6
  export * from "./Checkbox";
7
+ export * from "./Field";
7
8
  export * from "./Icon";
9
+ export * from "./KeyboardAvoidingScrollView";
8
10
  export * from "./ProgressCircle";
9
11
  export * from "./RadioGroup";
10
12
  export * from "./Stack";
11
13
  export * from "./Switch";
12
14
  export * from "./TagGroup";
13
15
  export * from "./Text";
16
+ export * from "./TextField";