@seed-design/lynx-react 0.3.0 → 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 (84) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +20 -0
  3. package/lib/components/Field/Field.d.ts +47 -0
  4. package/lib/components/Field/Field.d.ts.map +1 -0
  5. package/lib/components/Field/Field.jsx +121 -0
  6. package/lib/components/Field/Field.namespace.d.ts +2 -0
  7. package/lib/components/Field/Field.namespace.d.ts.map +1 -0
  8. package/lib/components/Field/Field.namespace.js +16 -0
  9. package/lib/components/Field/context.d.ts +19 -0
  10. package/lib/components/Field/context.d.ts.map +1 -0
  11. package/lib/components/Field/context.js +10 -0
  12. package/lib/components/Field/index.d.ts +3 -0
  13. package/lib/components/Field/index.d.ts.map +1 -0
  14. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts +42 -0
  15. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts.map +1 -0
  16. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.jsx +140 -0
  17. package/lib/components/KeyboardAvoidingScrollView/context.d.ts +17 -0
  18. package/lib/components/KeyboardAvoidingScrollView/context.d.ts.map +1 -0
  19. package/lib/components/KeyboardAvoidingScrollView/context.js +12 -0
  20. package/lib/components/KeyboardAvoidingScrollView/engine.d.ts +50 -0
  21. package/lib/components/KeyboardAvoidingScrollView/engine.d.ts.map +1 -0
  22. package/lib/components/KeyboardAvoidingScrollView/engine.js +259 -0
  23. package/lib/components/KeyboardAvoidingScrollView/geometry.d.ts +50 -0
  24. package/lib/components/KeyboardAvoidingScrollView/geometry.d.ts.map +1 -0
  25. package/lib/components/KeyboardAvoidingScrollView/geometry.js +90 -0
  26. package/lib/components/KeyboardAvoidingScrollView/index.d.ts +2 -0
  27. package/lib/components/KeyboardAvoidingScrollView/index.d.ts.map +1 -0
  28. package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.d.ts +11 -0
  29. package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.d.ts.map +1 -0
  30. package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.js +43 -0
  31. package/lib/components/KeyboardAvoidingScrollView/native-driver.d.ts +34 -0
  32. package/lib/components/KeyboardAvoidingScrollView/native-driver.d.ts.map +1 -0
  33. package/lib/components/KeyboardAvoidingScrollView/native-driver.js +133 -0
  34. package/lib/components/TextField/TextField.d.ts +113 -0
  35. package/lib/components/TextField/TextField.d.ts.map +1 -0
  36. package/lib/components/TextField/TextField.jsx +418 -0
  37. package/lib/components/TextField/TextField.namespace.d.ts +2 -0
  38. package/lib/components/TextField/TextField.namespace.d.ts.map +1 -0
  39. package/lib/components/TextField/TextField.namespace.js +14 -0
  40. package/lib/components/TextField/context.d.ts +22 -0
  41. package/lib/components/TextField/context.d.ts.map +1 -0
  42. package/lib/components/TextField/context.js +7 -0
  43. package/lib/components/TextField/index.d.ts +4 -0
  44. package/lib/components/TextField/index.d.ts.map +1 -0
  45. package/lib/components/TextField/useTextFieldWithGraphemes.d.ts +24 -0
  46. package/lib/components/TextField/useTextFieldWithGraphemes.d.ts.map +1 -0
  47. package/lib/components/TextField/useTextFieldWithGraphemes.js +47 -0
  48. package/lib/components/index.d.ts +3 -0
  49. package/lib/components/index.d.ts.map +1 -1
  50. package/lib/index.js +7 -1
  51. package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/_grapheme_data.js +8 -0
  52. package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/_incb_data.js +14 -0
  53. package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/core.js +68 -0
  54. package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/grapheme.js +175 -0
  55. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -1
  56. package/lib/utils/create-slot-recipe-context.jsx +7 -0
  57. package/package.json +10 -5
  58. package/src/components/Field/Field.namespace.ts +19 -0
  59. package/src/components/Field/Field.test.tsx +81 -0
  60. package/src/components/Field/Field.tsx +252 -0
  61. package/src/components/Field/context.ts +26 -0
  62. package/src/components/Field/index.ts +21 -0
  63. package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.test.tsx +267 -0
  64. package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.tsx +299 -0
  65. package/src/components/KeyboardAvoidingScrollView/context.ts +24 -0
  66. package/src/components/KeyboardAvoidingScrollView/engine.test.ts +451 -0
  67. package/src/components/KeyboardAvoidingScrollView/engine.ts +417 -0
  68. package/src/components/KeyboardAvoidingScrollView/geometry.test.ts +315 -0
  69. package/src/components/KeyboardAvoidingScrollView/geometry.ts +194 -0
  70. package/src/components/KeyboardAvoidingScrollView/index.ts +5 -0
  71. package/src/components/KeyboardAvoidingScrollView/keyboard-event-source.test.ts +122 -0
  72. package/src/components/KeyboardAvoidingScrollView/keyboard-event-source.ts +75 -0
  73. package/src/components/KeyboardAvoidingScrollView/native-driver.test.ts +223 -0
  74. package/src/components/KeyboardAvoidingScrollView/native-driver.ts +248 -0
  75. package/src/components/TextField/TextField.namespace.ts +16 -0
  76. package/src/components/TextField/TextField.test.tsx +924 -0
  77. package/src/components/TextField/TextField.tsx +912 -0
  78. package/src/components/TextField/context.ts +33 -0
  79. package/src/components/TextField/index.ts +23 -0
  80. package/src/components/TextField/useTextFieldWithGraphemes.test.tsx +80 -0
  81. package/src/components/TextField/useTextFieldWithGraphemes.ts +78 -0
  82. package/src/components/index.ts +3 -0
  83. package/src/utils/create-slot-recipe-context.test.tsx +51 -0
  84. package/src/utils/create-slot-recipe-context.tsx +23 -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";
@@ -29,6 +29,7 @@ const mockSlotRecipe = Object.assign(
29
29
  type MockProps = {
30
30
  children?: unknown;
31
31
  className?: string;
32
+ active?: boolean;
32
33
  };
33
34
 
34
35
  type MockReceived = { props?: MockProps };
@@ -93,4 +94,54 @@ describe("createSlotRecipeContext", () => {
93
94
  expect(rootReceived.props?.className).toBe("root-a extra");
94
95
  expect(childReceived.props?.className).toBe("label-a child-extra");
95
96
  });
97
+
98
+ it("restores root default props overwritten with undefined", () => {
99
+ const { Mock: Root, received } = createMock("Root");
100
+ const { withRootProvider } = createSlotRecipeContext(mockSlotRecipe);
101
+ const StyledRoot = withRootProvider<MockVariantProps & MockProps>(Root, {
102
+ defaultProps: { active: true },
103
+ });
104
+
105
+ render(<StyledRoot active={undefined} />);
106
+
107
+ expect(received.props?.active).toBe(true);
108
+ });
109
+
110
+ it("restores provider default props overwritten with undefined", () => {
111
+ const { Mock: Root, received } = createMock("Root");
112
+ const { withProvider } = createSlotRecipeContext(mockSlotRecipe);
113
+ const StyledRoot = withProvider<{ tag: "mock" }, MockVariantProps & MockProps>(Root, "root", {
114
+ defaultProps: { active: true },
115
+ });
116
+
117
+ render(<StyledRoot active={undefined} />);
118
+
119
+ expect(received.props?.active).toBe(true);
120
+ });
121
+
122
+ it("restores context default props overwritten with undefined and preserves false", () => {
123
+ const { Mock: Root } = createMock("Root");
124
+ const { Mock: Child, received } = createMock("Child");
125
+ const { withRootProvider, withContext } = createSlotRecipeContext(mockSlotRecipe);
126
+ const StyledRoot = withRootProvider<MockVariantProps & MockProps>(Root);
127
+ const StyledChild = withContext<{ tag: "mock" }, MockProps>(Child, "label", {
128
+ defaultProps: { active: true },
129
+ });
130
+
131
+ const { rerender } = render(
132
+ <StyledRoot>
133
+ <StyledChild active={undefined} />
134
+ </StyledRoot>,
135
+ );
136
+
137
+ expect(received.props?.active).toBe(true);
138
+
139
+ rerender(
140
+ <StyledRoot>
141
+ <StyledChild active={false} />
142
+ </StyledRoot>,
143
+ );
144
+
145
+ expect(received.props?.active).toBe(false);
146
+ });
96
147
  });
@@ -24,6 +24,20 @@ function assertNotIntrinsicComponent(Component: ElementType<any>, caller: string
24
24
  );
25
25
  }
26
26
 
27
+ function restoreDefaultProps<P>(
28
+ props: Record<string, unknown>,
29
+ defaultProps: Partial<P> | undefined,
30
+ ) {
31
+ if (!defaultProps) return;
32
+
33
+ // Explicit `undefined` should not override component-level defaults.
34
+ for (const key of Object.keys(defaultProps)) {
35
+ if (props[key] === undefined) {
36
+ props[key] = defaultProps[key as keyof P];
37
+ }
38
+ }
39
+ }
40
+
27
41
  export function createSlotRecipeContext<
28
42
  Props extends Record<string, string | boolean | undefined>,
29
43
  Classnames extends Record<string, string>,
@@ -93,6 +107,9 @@ export function createSlotRecipeContext<
93
107
  const StyledComponent = (innerProps: any) => {
94
108
  const props = { ...(defaultProps ?? {}), ...useProps(), ...innerProps } as Props &
95
109
  Record<string, unknown>;
110
+
111
+ restoreDefaultProps(props, defaultProps);
112
+
96
113
  const [variantProps, otherProps] = recipe.splitVariantProps(props);
97
114
  const classNames = recipe(variantProps);
98
115
 
@@ -123,6 +140,9 @@ export function createSlotRecipeContext<
123
140
 
124
141
  const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
125
142
  const props: any = { ...(defaultProps ?? {}), ...useProps(), ...innerProps };
143
+
144
+ restoreDefaultProps(props, defaultProps);
145
+
126
146
  const [variantProps, otherProps] = recipe.splitVariantProps(props as Props);
127
147
  const classNames = recipe(variantProps);
128
148
  const slotClassName: string | undefined = classNames[slot];
@@ -158,6 +178,9 @@ export function createSlotRecipeContext<
158
178
 
159
179
  const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
160
180
  const props: any = { ...(defaultProps ?? {}), ...innerProps };
181
+
182
+ restoreDefaultProps(props, defaultProps);
183
+
161
184
  const classNames = useClassNames();
162
185
  const slotClassName: string | undefined = slot ? classNames[slot] : undefined;
163
186
  const userClassName: string | undefined = props["className"];