@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.
- package/LICENSE +201 -0
- package/NOTICE +20 -0
- package/lib/components/Field/Field.d.ts +47 -0
- package/lib/components/Field/Field.d.ts.map +1 -0
- package/lib/components/Field/Field.jsx +121 -0
- package/lib/components/Field/Field.namespace.d.ts +2 -0
- package/lib/components/Field/Field.namespace.d.ts.map +1 -0
- package/lib/components/Field/Field.namespace.js +16 -0
- package/lib/components/Field/context.d.ts +19 -0
- package/lib/components/Field/context.d.ts.map +1 -0
- package/lib/components/Field/context.js +10 -0
- package/lib/components/Field/index.d.ts +3 -0
- package/lib/components/Field/index.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts +42 -0
- package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.jsx +140 -0
- package/lib/components/KeyboardAvoidingScrollView/context.d.ts +17 -0
- package/lib/components/KeyboardAvoidingScrollView/context.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/context.js +12 -0
- package/lib/components/KeyboardAvoidingScrollView/engine.d.ts +50 -0
- package/lib/components/KeyboardAvoidingScrollView/engine.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/engine.js +259 -0
- package/lib/components/KeyboardAvoidingScrollView/geometry.d.ts +50 -0
- package/lib/components/KeyboardAvoidingScrollView/geometry.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/geometry.js +90 -0
- package/lib/components/KeyboardAvoidingScrollView/index.d.ts +2 -0
- package/lib/components/KeyboardAvoidingScrollView/index.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.d.ts +11 -0
- package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/keyboard-event-source.js +43 -0
- package/lib/components/KeyboardAvoidingScrollView/native-driver.d.ts +34 -0
- package/lib/components/KeyboardAvoidingScrollView/native-driver.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/native-driver.js +133 -0
- package/lib/components/TextField/TextField.d.ts +113 -0
- package/lib/components/TextField/TextField.d.ts.map +1 -0
- package/lib/components/TextField/TextField.jsx +418 -0
- package/lib/components/TextField/TextField.namespace.d.ts +2 -0
- package/lib/components/TextField/TextField.namespace.d.ts.map +1 -0
- package/lib/components/TextField/TextField.namespace.js +14 -0
- package/lib/components/TextField/context.d.ts +22 -0
- package/lib/components/TextField/context.d.ts.map +1 -0
- package/lib/components/TextField/context.js +7 -0
- package/lib/components/TextField/index.d.ts +4 -0
- package/lib/components/TextField/index.d.ts.map +1 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.d.ts +24 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.d.ts.map +1 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.js +47 -0
- package/lib/components/index.d.ts +3 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/index.js +7 -1
- package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/_grapheme_data.js +8 -0
- package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/_incb_data.js +14 -0
- package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/core.js +68 -0
- package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/grapheme.js +175 -0
- package/lib/utils/create-slot-recipe-context.d.ts.map +1 -1
- package/lib/utils/create-slot-recipe-context.jsx +7 -0
- package/package.json +10 -5
- package/src/components/Field/Field.namespace.ts +19 -0
- package/src/components/Field/Field.test.tsx +81 -0
- package/src/components/Field/Field.tsx +252 -0
- package/src/components/Field/context.ts +26 -0
- package/src/components/Field/index.ts +21 -0
- package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.test.tsx +267 -0
- package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.tsx +299 -0
- package/src/components/KeyboardAvoidingScrollView/context.ts +24 -0
- package/src/components/KeyboardAvoidingScrollView/engine.test.ts +451 -0
- package/src/components/KeyboardAvoidingScrollView/engine.ts +417 -0
- package/src/components/KeyboardAvoidingScrollView/geometry.test.ts +315 -0
- package/src/components/KeyboardAvoidingScrollView/geometry.ts +194 -0
- package/src/components/KeyboardAvoidingScrollView/index.ts +5 -0
- package/src/components/KeyboardAvoidingScrollView/keyboard-event-source.test.ts +122 -0
- package/src/components/KeyboardAvoidingScrollView/keyboard-event-source.ts +75 -0
- package/src/components/KeyboardAvoidingScrollView/native-driver.test.ts +223 -0
- package/src/components/KeyboardAvoidingScrollView/native-driver.ts +248 -0
- package/src/components/TextField/TextField.namespace.ts +16 -0
- package/src/components/TextField/TextField.test.tsx +924 -0
- package/src/components/TextField/TextField.tsx +912 -0
- package/src/components/TextField/context.ts +33 -0
- package/src/components/TextField/index.ts +23 -0
- package/src/components/TextField/useTextFieldWithGraphemes.test.tsx +80 -0
- package/src/components/TextField/useTextFieldWithGraphemes.ts +78 -0
- package/src/components/index.ts +3 -0
- package/src/utils/create-slot-recipe-context.test.tsx +51 -0
- package/src/utils/create-slot-recipe-context.tsx +23 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
type ForwardRefExoticComponent,
|
|
8
|
+
type PropsWithoutRef,
|
|
9
|
+
type RefAttributes,
|
|
10
|
+
} from "@lynx-js/react";
|
|
11
|
+
import type { IntrinsicElements, NodesRef } from "@lynx-js/types";
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
LynxAccessibilityProps,
|
|
15
|
+
LynxStyledElementProps,
|
|
16
|
+
LynxTouchProps,
|
|
17
|
+
LynxViewProps,
|
|
18
|
+
LynxViewRef,
|
|
19
|
+
} from "../../types";
|
|
20
|
+
import { KeyboardAvoidanceActionsContext, type KeyboardAvoidanceActions } from "./context";
|
|
21
|
+
import { createKeyboardAvoidingEngine, type KeyboardAvoidingScheduler } from "./engine";
|
|
22
|
+
import { lynxKeyboardEventSource } from "./keyboard-event-source";
|
|
23
|
+
import { lynxKeyboardAvoidingNativeDriver } from "./native-driver";
|
|
24
|
+
|
|
25
|
+
type NativeScrollViewProps = IntrinsicElements["scroll-view"];
|
|
26
|
+
type LayoutChangeHandler = NonNullable<NativeScrollViewProps["bindlayoutchange"]>;
|
|
27
|
+
type ScrollHandler = NonNullable<NativeScrollViewProps["bindscroll"]>;
|
|
28
|
+
type ScrollEndHandler = NonNullable<NativeScrollViewProps["bindscrollend"]>;
|
|
29
|
+
type TouchStartHandler = NonNullable<NativeScrollViewProps["bindtouchstart"]>;
|
|
30
|
+
type TouchEndHandler = NonNullable<NativeScrollViewProps["bindtouchend"]>;
|
|
31
|
+
type TouchCancelHandler = NonNullable<NativeScrollViewProps["bindtouchcancel"]>;
|
|
32
|
+
type LynxForwardRefComponent<T, P> = ForwardRefExoticComponent<
|
|
33
|
+
PropsWithoutRef<P> & RefAttributes<T>
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
export type KeyboardAvoidingScrollBehavior = "smooth" | "instant";
|
|
37
|
+
|
|
38
|
+
const lynxKeyboardAvoidingScheduler: KeyboardAvoidingScheduler = {
|
|
39
|
+
scheduleFrame(callback) {
|
|
40
|
+
"background only";
|
|
41
|
+
|
|
42
|
+
const frame = requestAnimationFrame(() => {
|
|
43
|
+
"background only";
|
|
44
|
+
void callback();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
"background only";
|
|
49
|
+
cancelAnimationFrame(frame);
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
scheduleTimer(callback, delayMs) {
|
|
53
|
+
"background only";
|
|
54
|
+
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
"background only";
|
|
57
|
+
void callback();
|
|
58
|
+
}, delayMs);
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
"background only";
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const spacerProps = {
|
|
68
|
+
"accessibility-elements-hidden": true,
|
|
69
|
+
flatten: false,
|
|
70
|
+
} satisfies LynxViewProps;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @platform Lynx
|
|
74
|
+
*
|
|
75
|
+
* 세로 `<scroll-view>` 안의 활성 입력 요소가 키보드에 가려지지 않도록 한다.
|
|
76
|
+
* `TextField.Input`과 `TextField.Textarea`는 focus 시 자동으로 이 컴포넌트에 등록된다.
|
|
77
|
+
*
|
|
78
|
+
* 현재 미지원 기능:
|
|
79
|
+
* - 가로 스크롤과 중첩 스크롤
|
|
80
|
+
* - 키보드 툴바 높이 반영
|
|
81
|
+
* - 큰 Textarea의 caret 단위 회피
|
|
82
|
+
*/
|
|
83
|
+
export interface KeyboardAvoidingScrollViewProps
|
|
84
|
+
extends LynxStyledElementProps,
|
|
85
|
+
LynxAccessibilityProps,
|
|
86
|
+
LynxTouchProps {
|
|
87
|
+
id?: NativeScrollViewProps["id"];
|
|
88
|
+
hidden?: NativeScrollViewProps["hidden"];
|
|
89
|
+
focusable?: NativeScrollViewProps["focusable"];
|
|
90
|
+
bounces?: NativeScrollViewProps["bounces"];
|
|
91
|
+
"enable-scroll"?: NativeScrollViewProps["enable-scroll"];
|
|
92
|
+
"scroll-bar-enable"?: NativeScrollViewProps["scroll-bar-enable"];
|
|
93
|
+
"upper-threshold"?: NativeScrollViewProps["upper-threshold"];
|
|
94
|
+
"lower-threshold"?: NativeScrollViewProps["lower-threshold"];
|
|
95
|
+
"initial-scroll-offset"?: NativeScrollViewProps["initial-scroll-offset"];
|
|
96
|
+
"initial-scroll-to-index"?: NativeScrollViewProps["initial-scroll-to-index"];
|
|
97
|
+
bindlayoutchange?: NativeScrollViewProps["bindlayoutchange"];
|
|
98
|
+
bindscrolltoupper?: NativeScrollViewProps["bindscrolltoupper"];
|
|
99
|
+
bindscrolltolower?: NativeScrollViewProps["bindscrolltolower"];
|
|
100
|
+
bindscroll?: NativeScrollViewProps["bindscroll"];
|
|
101
|
+
bindscrollend?: NativeScrollViewProps["bindscrollend"];
|
|
102
|
+
bindcontentsizechanged?: NativeScrollViewProps["bindcontentsizechanged"];
|
|
103
|
+
/** 키보드와 활성 입력 요소 사이에 확보할 간격(px). @defaultValue 24 */
|
|
104
|
+
keyboardGap?: number;
|
|
105
|
+
/** 회피 위치로 이동할 때 사용할 스크롤 방식. @defaultValue "smooth" */
|
|
106
|
+
scrollBehavior?: KeyboardAvoidingScrollBehavior;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const KeyboardAvoidingScrollView: LynxForwardRefComponent<
|
|
110
|
+
NodesRef,
|
|
111
|
+
KeyboardAvoidingScrollViewProps
|
|
112
|
+
> = forwardRef<NodesRef, KeyboardAvoidingScrollViewProps>((props, forwardedRef) => {
|
|
113
|
+
const {
|
|
114
|
+
children,
|
|
115
|
+
keyboardGap = 24,
|
|
116
|
+
scrollBehavior = "smooth",
|
|
117
|
+
bindlayoutchange: userBindLayoutChange,
|
|
118
|
+
bindscroll: userBindScroll,
|
|
119
|
+
bindscrollend: userBindScrollEnd,
|
|
120
|
+
bindtouchstart: userBindTouchStart,
|
|
121
|
+
bindtouchend: userBindTouchEnd,
|
|
122
|
+
bindtouchcancel: userBindTouchCancel,
|
|
123
|
+
...nativeProps
|
|
124
|
+
} = props;
|
|
125
|
+
const scrollRef = useRef<NodesRef>(null);
|
|
126
|
+
const spacerRef = useRef<NodesRef>(null);
|
|
127
|
+
const keyboardGapRef = useRef(keyboardGap);
|
|
128
|
+
const committedKeyboardGapRef = useRef(keyboardGap);
|
|
129
|
+
const scrollBehaviorRef = useRef(scrollBehavior);
|
|
130
|
+
const touchActiveRef = useRef(false);
|
|
131
|
+
const didScrollDuringTouchRef = useRef(false);
|
|
132
|
+
|
|
133
|
+
keyboardGapRef.current = keyboardGap;
|
|
134
|
+
scrollBehaviorRef.current = scrollBehavior;
|
|
135
|
+
|
|
136
|
+
const engine = useMemo(
|
|
137
|
+
() =>
|
|
138
|
+
createKeyboardAvoidingEngine({
|
|
139
|
+
driver: lynxKeyboardAvoidingNativeDriver,
|
|
140
|
+
scheduler: lynxKeyboardAvoidingScheduler,
|
|
141
|
+
getScrollNode: () => scrollRef.current,
|
|
142
|
+
getSpacerNode: () => spacerRef.current,
|
|
143
|
+
getKeyboardGap: () => keyboardGapRef.current,
|
|
144
|
+
getToolbarHeight: () => 0,
|
|
145
|
+
getSmooth: () => scrollBehaviorRef.current === "smooth",
|
|
146
|
+
}),
|
|
147
|
+
[],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const mergedRef = useCallback(
|
|
151
|
+
(node: NodesRef | null) => {
|
|
152
|
+
"background only";
|
|
153
|
+
|
|
154
|
+
scrollRef.current = node;
|
|
155
|
+
if (typeof forwardedRef === "function") {
|
|
156
|
+
forwardedRef(node);
|
|
157
|
+
} else if (forwardedRef) {
|
|
158
|
+
forwardedRef.current = node;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
[forwardedRef],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const actions = useMemo<KeyboardAvoidanceActions>(
|
|
165
|
+
() => ({
|
|
166
|
+
focus(registration) {
|
|
167
|
+
"background only";
|
|
168
|
+
engine.focus(registration);
|
|
169
|
+
},
|
|
170
|
+
blur(owner) {
|
|
171
|
+
"background only";
|
|
172
|
+
engine.blur(owner);
|
|
173
|
+
},
|
|
174
|
+
layoutChanged(owner) {
|
|
175
|
+
"background only";
|
|
176
|
+
engine.layoutChanged(owner);
|
|
177
|
+
},
|
|
178
|
+
unregister(owner) {
|
|
179
|
+
"background only";
|
|
180
|
+
engine.unregister(owner);
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
[engine],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const unsubscribe = lynxKeyboardEventSource.subscribe((state) => {
|
|
188
|
+
"background only";
|
|
189
|
+
engine.keyboardChanged(state);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return () => {
|
|
193
|
+
unsubscribe();
|
|
194
|
+
engine.dispose();
|
|
195
|
+
};
|
|
196
|
+
}, [engine]);
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (Object.is(committedKeyboardGapRef.current, keyboardGap)) return;
|
|
200
|
+
|
|
201
|
+
committedKeyboardGapRef.current = keyboardGap;
|
|
202
|
+
engine.viewportChanged();
|
|
203
|
+
}, [engine, keyboardGap]);
|
|
204
|
+
|
|
205
|
+
const handleLayoutChange = useCallback<LayoutChangeHandler>(
|
|
206
|
+
(...args) => {
|
|
207
|
+
"background only";
|
|
208
|
+
|
|
209
|
+
engine.viewportChanged();
|
|
210
|
+
userBindLayoutChange?.(...args);
|
|
211
|
+
},
|
|
212
|
+
[engine, userBindLayoutChange],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const handleTouchStart = useCallback<TouchStartHandler>(
|
|
216
|
+
(...args) => {
|
|
217
|
+
"background only";
|
|
218
|
+
|
|
219
|
+
touchActiveRef.current = true;
|
|
220
|
+
didScrollDuringTouchRef.current = false;
|
|
221
|
+
engine.userScrollStarted();
|
|
222
|
+
userBindTouchStart?.(...args);
|
|
223
|
+
},
|
|
224
|
+
[engine, userBindTouchStart],
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const handleScroll = useCallback<ScrollHandler>(
|
|
228
|
+
(...args) => {
|
|
229
|
+
"background only";
|
|
230
|
+
|
|
231
|
+
if (touchActiveRef.current) {
|
|
232
|
+
didScrollDuringTouchRef.current = true;
|
|
233
|
+
}
|
|
234
|
+
userBindScroll?.(...args);
|
|
235
|
+
},
|
|
236
|
+
[userBindScroll],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const handleTouchEnd = useCallback<TouchEndHandler>(
|
|
240
|
+
(...args) => {
|
|
241
|
+
"background only";
|
|
242
|
+
|
|
243
|
+
touchActiveRef.current = false;
|
|
244
|
+
if (!didScrollDuringTouchRef.current) {
|
|
245
|
+
engine.userScrollEnded();
|
|
246
|
+
}
|
|
247
|
+
userBindTouchEnd?.(...args);
|
|
248
|
+
},
|
|
249
|
+
[engine, userBindTouchEnd],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const handleTouchCancel = useCallback<TouchCancelHandler>(
|
|
253
|
+
(...args) => {
|
|
254
|
+
"background only";
|
|
255
|
+
|
|
256
|
+
touchActiveRef.current = false;
|
|
257
|
+
didScrollDuringTouchRef.current = false;
|
|
258
|
+
engine.userScrollEnded();
|
|
259
|
+
userBindTouchCancel?.(...args);
|
|
260
|
+
},
|
|
261
|
+
[engine, userBindTouchCancel],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const handleScrollEnd = useCallback<ScrollEndHandler>(
|
|
265
|
+
(...args) => {
|
|
266
|
+
"background only";
|
|
267
|
+
|
|
268
|
+
touchActiveRef.current = false;
|
|
269
|
+
didScrollDuringTouchRef.current = false;
|
|
270
|
+
engine.userScrollEnded();
|
|
271
|
+
userBindScrollEnd?.(...args);
|
|
272
|
+
},
|
|
273
|
+
[engine, userBindScrollEnd],
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<KeyboardAvoidanceActionsContext.Provider value={actions}>
|
|
278
|
+
<scroll-view
|
|
279
|
+
{...nativeProps}
|
|
280
|
+
ref={mergedRef}
|
|
281
|
+
scroll-orientation="vertical"
|
|
282
|
+
flatten={false}
|
|
283
|
+
bindlayoutchange={handleLayoutChange}
|
|
284
|
+
bindtouchstart={handleTouchStart}
|
|
285
|
+
bindtouchend={handleTouchEnd}
|
|
286
|
+
bindtouchcancel={handleTouchCancel}
|
|
287
|
+
bindscroll={handleScroll}
|
|
288
|
+
bindscrollend={handleScrollEnd}
|
|
289
|
+
>
|
|
290
|
+
{children}
|
|
291
|
+
<view
|
|
292
|
+
{...({ ref: spacerRef as LynxViewRef } as Record<string, unknown>)}
|
|
293
|
+
{...spacerProps}
|
|
294
|
+
/>
|
|
295
|
+
</scroll-view>
|
|
296
|
+
</KeyboardAvoidanceActionsContext.Provider>
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
KeyboardAvoidingScrollView.displayName = "KeyboardAvoidingScrollView";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createContext, useContext, type Context } from "@lynx-js/react";
|
|
2
|
+
import type { NodesRef } from "@lynx-js/types";
|
|
3
|
+
|
|
4
|
+
import type { KeyboardAvoidanceRegistration as EngineRegistration } from "./engine";
|
|
5
|
+
|
|
6
|
+
export type KeyboardAvoidanceRegistration = EngineRegistration<NodesRef>;
|
|
7
|
+
|
|
8
|
+
export interface KeyboardAvoidanceActions {
|
|
9
|
+
focus(registration: KeyboardAvoidanceRegistration): void;
|
|
10
|
+
blur(owner: object): void;
|
|
11
|
+
layoutChanged(owner: object): void;
|
|
12
|
+
unregister(owner: object): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const KeyboardAvoidanceActionsContext: Context<KeyboardAvoidanceActions | null> =
|
|
16
|
+
createContext<KeyboardAvoidanceActions | null>(null);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lynx TextField가 선택적으로 소비하는 내부 연결점이다.
|
|
20
|
+
* Provider 밖에서는 `null`을 반환해 TextField 자체 동작을 방해하지 않는다.
|
|
21
|
+
*/
|
|
22
|
+
export function useKeyboardAvoidanceActions(): KeyboardAvoidanceActions | null {
|
|
23
|
+
return useContext(KeyboardAvoidanceActionsContext);
|
|
24
|
+
}
|