@seed-design/lynx-react 0.3.1 → 0.4.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/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/ProgressCircle/ProgressCircle.d.ts.map +1 -1
- package/lib/components/ProgressCircle/ProgressCircle.jsx +36 -5
- package/lib/components/Stack/Stack.d.ts.map +1 -1
- package/lib/components/Stack/Stack.jsx +10 -1
- 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 +2 -1
- package/lib/components/TextField/TextField.d.ts +131 -0
- package/lib/components/TextField/TextField.d.ts.map +1 -0
- package/lib/components/TextField/TextField.jsx +434 -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/package.json +7 -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/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/TagGroup/TagGroup.test.tsx +73 -0
- package/src/components/TagGroup/TagGroup.tsx +4 -2
- package/src/components/TextField/TextField.namespace.ts +16 -0
- package/src/components/TextField/TextField.test.tsx +1052 -0
- package/src/components/TextField/TextField.tsx +954 -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/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/_incb_data.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { decodeUnicodeData } from "./core.js";
|
|
2
|
+
//#region ../../node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/_incb_data.js
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./core.js').UnicodeRange} UnicodeRange
|
|
5
|
+
* @typedef {import('./core.js').UnicodeDataEncoding} UnicodeDataEncoding
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The Unicode `Indic_Conjunct_Break=Consonant` derived property table
|
|
9
|
+
*
|
|
10
|
+
* @type {UnicodeRange[]}
|
|
11
|
+
*/
|
|
12
|
+
var consonant_ranges = decodeUnicodeData("1sl,10,1ug,7,1vc,7,1w5,j,1wq,6,1wy,,1x2,3,1y4,1,1y7,,1yo,1,239,j,23u,6,242,1,245,4,261,,26t,j,27e,6,27m,1,27p,4,28s,1,28v,,29d,,2dx,j,2ei,f,2fs,2,2l1,11");
|
|
13
|
+
//#endregion
|
|
14
|
+
export { consonant_ranges };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region ../../node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/core.js
|
|
2
|
+
/**
|
|
3
|
+
* @template {number} [T=number]
|
|
4
|
+
* @typedef {[from: number, to: number, category: T]} CategorizedUnicodeRange
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {CategorizedUnicodeRange<0>} UnicodeRange
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {string & { __tag: 'UnicodeDataEncoding' }} UnicodeDataEncoding
|
|
11
|
+
*
|
|
12
|
+
* Encoding for array of {@link UnicodeRange}, items separated by comma.
|
|
13
|
+
*
|
|
14
|
+
* Each {@link UnicodeDataRow} packed as a base36 integer:
|
|
15
|
+
*
|
|
16
|
+
* padding = to - from
|
|
17
|
+
* encoding = base36(from) + ',' + base36(padding)
|
|
18
|
+
*
|
|
19
|
+
* Notes:
|
|
20
|
+
* - base36 can hold surprisingly large numbers in a few characters.
|
|
21
|
+
* - The biggest codepoint is 0xE01F0 (918,000) at this point
|
|
22
|
+
* - The max value of a category is 23; https://www.unicode.org/reports/tr29/tr29-45.html#Table_Word_Break_Property_Values
|
|
23
|
+
* - The longest range is 42,720; CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6DF
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* @template {number} [T=number]
|
|
27
|
+
* @param {UnicodeDataEncoding} data
|
|
28
|
+
* @param {string} [cats='']
|
|
29
|
+
* @returns {Array<CategorizedUnicodeRange<T>>}
|
|
30
|
+
*/
|
|
31
|
+
function decodeUnicodeData(data, cats = "") {
|
|
32
|
+
let buf = [], nums = data.split(",").map((s) => s ? parseInt(s, 36) : 0), n = 0;
|
|
33
|
+
for (let i = 0; i < nums.length; i++) i % 2 ? buf.push([
|
|
34
|
+
n,
|
|
35
|
+
n + nums[i],
|
|
36
|
+
cats ? parseInt(cats[i >> 1], 36) : 0
|
|
37
|
+
]) : n = nums[i];
|
|
38
|
+
return buf;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @template {object} Ext
|
|
42
|
+
* @typedef {{
|
|
43
|
+
* segment: string,
|
|
44
|
+
* index: number,
|
|
45
|
+
* input: string,
|
|
46
|
+
* } & Ext} SegmentOutput
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* @template {object} T
|
|
50
|
+
* @typedef {IterableIterator<SegmentOutput<T>>} Segmenter
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* @template {number} [T=number]
|
|
54
|
+
* @param {number} cp
|
|
55
|
+
* @param {CategorizedUnicodeRange<T>[]} ranges
|
|
56
|
+
* @return {number} index of matched unicode range, or -1 if no match
|
|
57
|
+
*/
|
|
58
|
+
function findUnicodeRangeIndex(cp, ranges, lo = 0, hi = ranges.length - 1) {
|
|
59
|
+
while (lo <= hi) {
|
|
60
|
+
let mid = lo + hi >>> 1, range = ranges[mid];
|
|
61
|
+
if (cp < range[0]) hi = mid - 1;
|
|
62
|
+
else if (cp > range[1]) lo = mid + 1;
|
|
63
|
+
else return mid;
|
|
64
|
+
}
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { decodeUnicodeData, findUnicodeRangeIndex };
|
package/lib/node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/grapheme.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { findUnicodeRangeIndex } from "./core.js";
|
|
2
|
+
import { grapheme_ranges } from "./_grapheme_data.js";
|
|
3
|
+
import { consonant_ranges } from "./_incb_data.js";
|
|
4
|
+
//#region ../../node_modules/.bun/unicode-segmenter@0.14.5/node_modules/unicode-segmenter/grapheme.js
|
|
5
|
+
var BMP_MAX = 65535;
|
|
6
|
+
/**
|
|
7
|
+
* Unicode segmentation by extended grapheme rules.
|
|
8
|
+
*
|
|
9
|
+
* This is fully compatible with the {@link Intl.Segmenter.segment} API
|
|
10
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment
|
|
11
|
+
*
|
|
12
|
+
* @param {string} input
|
|
13
|
+
* @return {GraphemeSegmenter} iterator for grapheme cluster segments
|
|
14
|
+
*/
|
|
15
|
+
function* graphemeSegments(input) {
|
|
16
|
+
let cp = input.codePointAt(0);
|
|
17
|
+
if (cp == null) return;
|
|
18
|
+
/** Current cursor position. */
|
|
19
|
+
let cursor = cp <= BMP_MAX ? 1 : 2;
|
|
20
|
+
/** Total length of the input string. */
|
|
21
|
+
let len = input.length;
|
|
22
|
+
/** Category of codepoint immediately preceding cursor */
|
|
23
|
+
let catBefore = cat(cp);
|
|
24
|
+
/** @type {GraphemeCategoryNum} Category of codepoint immediately preceding cursor. */
|
|
25
|
+
let catAfter = 0;
|
|
26
|
+
/** The number of RIS codepoints preceding `cursor`. */
|
|
27
|
+
let risCount = 0;
|
|
28
|
+
/**
|
|
29
|
+
* Emoji state for GB11: tracks if we've seen Extended_Pictographic followed by Extend* ZWJ
|
|
30
|
+
* Only relevant when catBefore === ZWJ && catAfter === Extended_Pictographic
|
|
31
|
+
*/
|
|
32
|
+
let emoji = false;
|
|
33
|
+
/** InCB=Consonant - segment started with Indic consonant */
|
|
34
|
+
let consonant = false;
|
|
35
|
+
/** InCB=Linker - seen a linker after consonant */
|
|
36
|
+
let linker = false;
|
|
37
|
+
let index = 0;
|
|
38
|
+
/** Beginning category of a segment */
|
|
39
|
+
let _catBegin = catBefore;
|
|
40
|
+
/** Memoize the beginning code point of the segment. */
|
|
41
|
+
let _hd = cp;
|
|
42
|
+
while (cursor < len) {
|
|
43
|
+
cp = input.codePointAt(cursor);
|
|
44
|
+
catAfter = cat(cp);
|
|
45
|
+
let boundary = true;
|
|
46
|
+
if (catBefore === 1) boundary = catAfter !== 6;
|
|
47
|
+
else if (catBefore === 2 || catBefore === 6) boundary = true;
|
|
48
|
+
else if (catAfter === 1 || catAfter === 2 || catAfter === 6) boundary = true;
|
|
49
|
+
else if (catAfter === 3 || catAfter === 14 || catAfter === 11) boundary = false;
|
|
50
|
+
else if (catBefore === 9) boundary = false;
|
|
51
|
+
else if (catBefore === 14 && catAfter === 4) boundary = !emoji;
|
|
52
|
+
else if (catBefore === 10 && catAfter === 10) boundary = risCount++ % 2 === 1;
|
|
53
|
+
else if (catBefore === 5) boundary = !(catAfter === 5 || catAfter === 13 || catAfter === 7 || catAfter === 8);
|
|
54
|
+
else if ((catBefore === 7 || catBefore === 13) && (catAfter === 13 || catAfter === 12)) boundary = false;
|
|
55
|
+
else if ((catBefore === 8 || catBefore === 12) && catAfter === 12) boundary = false;
|
|
56
|
+
else if (catAfter === 0 && consonant && linker && isIndicConjunctConsonant(cp)) boundary = false;
|
|
57
|
+
if (boundary) {
|
|
58
|
+
yield {
|
|
59
|
+
segment: input.slice(index, cursor),
|
|
60
|
+
index,
|
|
61
|
+
input,
|
|
62
|
+
_hd,
|
|
63
|
+
_catBegin,
|
|
64
|
+
_catEnd: catBefore
|
|
65
|
+
};
|
|
66
|
+
emoji = false;
|
|
67
|
+
risCount = 0;
|
|
68
|
+
index = cursor;
|
|
69
|
+
_catBegin = catAfter;
|
|
70
|
+
_hd = cp;
|
|
71
|
+
} else if (catAfter === 14 && (catBefore === 3 || catBefore === 4)) emoji = true;
|
|
72
|
+
else if (cp >= 2325) {
|
|
73
|
+
if (!consonant && catBefore === 0) consonant = isIndicConjunctConsonant(_hd);
|
|
74
|
+
if (consonant && catAfter === 3) linker = linker || cp === 2381 || cp === 2509 || cp === 2637 || cp === 2765 || cp === 2893 || cp === 3149 || cp === 3405;
|
|
75
|
+
else linker = false;
|
|
76
|
+
}
|
|
77
|
+
cursor += cp <= BMP_MAX ? 1 : 2;
|
|
78
|
+
catBefore = catAfter;
|
|
79
|
+
}
|
|
80
|
+
if (index < len) yield {
|
|
81
|
+
segment: input.slice(index),
|
|
82
|
+
index,
|
|
83
|
+
input,
|
|
84
|
+
_hd,
|
|
85
|
+
_catBegin,
|
|
86
|
+
_catEnd: catBefore
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Split given text into extended grapheme clusters.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} text
|
|
93
|
+
* @return {IterableIterator<string>} iterator for grapheme clusters
|
|
94
|
+
*
|
|
95
|
+
* @see {@link graphemeSegments} if you need extra information.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* [...splitGraphemes('abc')] // => ['a', 'b', 'c']
|
|
99
|
+
*/
|
|
100
|
+
function* splitGraphemes(text) {
|
|
101
|
+
for (let s of graphemeSegments(text)) yield s.segment;
|
|
102
|
+
}
|
|
103
|
+
var SEG0 = new Uint8Array(6080), SEG0_MIN = 128, SEG0_MAX = 12287;
|
|
104
|
+
var SEG1 = new Uint8Array(1536), SEG1_MIN = 40960, SEG1_MAX = 44031;
|
|
105
|
+
var SEG_CURSOR = (() => {
|
|
106
|
+
let cursor = 0;
|
|
107
|
+
while (true) {
|
|
108
|
+
let [start, end, cat] = grapheme_ranges[cursor];
|
|
109
|
+
if (start > SEG1_MAX) break;
|
|
110
|
+
cursor++;
|
|
111
|
+
if (end < SEG0_MIN || start > SEG0_MAX && end < SEG1_MIN) continue;
|
|
112
|
+
for (let cp = start; cp <= end; cp++) {
|
|
113
|
+
let seg, idx = 0;
|
|
114
|
+
if (cp <= SEG0_MAX) {
|
|
115
|
+
seg = SEG0;
|
|
116
|
+
idx = cp - SEG0_MIN >> 1;
|
|
117
|
+
} else {
|
|
118
|
+
seg = SEG1;
|
|
119
|
+
idx = cp - SEG1_MIN >> 1;
|
|
120
|
+
}
|
|
121
|
+
seg[idx] = cp & 1 ? seg[idx] & 15 | cat << 4 : seg[idx] & 240 | cat;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return cursor;
|
|
125
|
+
})();
|
|
126
|
+
/**
|
|
127
|
+
* `Grapheme_Cluster_Break` property value of a given codepoint
|
|
128
|
+
*
|
|
129
|
+
* @see https://www.unicode.org/reports/tr29/tr29-43.html#Default_Grapheme_Cluster_Table
|
|
130
|
+
*
|
|
131
|
+
* @param {number} cp
|
|
132
|
+
* @return {GraphemeCategoryNum}
|
|
133
|
+
*/
|
|
134
|
+
function cat(cp) {
|
|
135
|
+
if (cp < SEG0_MIN) {
|
|
136
|
+
if (cp >= 32) return 0;
|
|
137
|
+
if (cp === 10) return 6;
|
|
138
|
+
if (cp === 13) return 1;
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
if (cp <= SEG0_MAX) {
|
|
142
|
+
let byte = SEG0[cp - SEG0_MIN >> 1];
|
|
143
|
+
return cp & 1 ? byte >> 4 : byte & 15;
|
|
144
|
+
}
|
|
145
|
+
if (cp < SEG1_MIN) {
|
|
146
|
+
if (cp < 12336) return cp >= 12330 ? 3 : 0;
|
|
147
|
+
if (cp < 12443) {
|
|
148
|
+
if (cp === 12336 || cp === 12349) return 4;
|
|
149
|
+
return cp >= 12441 ? 3 : 0;
|
|
150
|
+
}
|
|
151
|
+
if (cp === 12951 || cp === 12953) return 4;
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
if (cp <= SEG1_MAX) {
|
|
155
|
+
let byte = SEG1[cp - SEG1_MIN >> 1];
|
|
156
|
+
return cp & 1 ? byte >> 4 : byte & 15;
|
|
157
|
+
}
|
|
158
|
+
if (cp <= 55203) return (cp - 44032) % 28 === 0 ? 7 : 8;
|
|
159
|
+
if (cp <= 55295) {
|
|
160
|
+
if (cp <= 55238) return cp >= 55216 ? 13 : 0;
|
|
161
|
+
return cp >= 55243 ? 12 : 0;
|
|
162
|
+
}
|
|
163
|
+
if (cp < 65024) return cp === 64286 ? 3 : 0;
|
|
164
|
+
let idx = findUnicodeRangeIndex(cp, grapheme_ranges, SEG_CURSOR);
|
|
165
|
+
return idx < 0 ? 0 : grapheme_ranges[idx][2];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* @param {number} cp
|
|
169
|
+
* @return {boolean}
|
|
170
|
+
*/
|
|
171
|
+
function isIndicConjunctConsonant(cp) {
|
|
172
|
+
return findUnicodeRangeIndex(cp, consonant_ranges) >= 0;
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { splitGraphemes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-design/lynx-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/daangn/seed-design.git",
|
|
@@ -34,14 +34,15 @@
|
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@lynx-js/react": ">=0.117.0",
|
|
36
36
|
"@lynx-js/types": ">=3.7.0",
|
|
37
|
-
"@seed-design/lynx-css": "0.0.0 || >=0.
|
|
37
|
+
"@seed-design/lynx-css": "0.0.0 || >=0.8.0 <1.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@lynx-js/react": "^0.117.0",
|
|
41
41
|
"@lynx-js/types": "^3.7.0",
|
|
42
|
-
"@seed-design/lynx-css": "0.
|
|
42
|
+
"@seed-design/lynx-css": "0.8.1",
|
|
43
43
|
"@testing-library/jest-dom": "^6.9.1",
|
|
44
44
|
"jsdom": "^29.0.2",
|
|
45
|
+
"preact": "npm:@lynx-js/internal-preact@10.28.4-ee7bb26",
|
|
45
46
|
"vite": "^8.0.0",
|
|
46
47
|
"vite-plugin-dts": "^4.5.4",
|
|
47
48
|
"vitest": "^3.2.4"
|
|
@@ -50,8 +51,9 @@
|
|
|
50
51
|
"access": "public"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@lynx-js/lynx-ui-sheet": "^3.130.1",
|
|
54
54
|
"@lynx-js/lynx-ui-common": "^3.130.0",
|
|
55
|
-
"
|
|
55
|
+
"@lynx-js/lynx-ui-sheet": "^3.130.1",
|
|
56
|
+
"clsx": "^2.1.1",
|
|
57
|
+
"unicode-segmenter": "^0.14.4"
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
FieldCharacterCount as CharacterCount,
|
|
3
|
+
FieldDescription as Description,
|
|
4
|
+
FieldErrorMessage as ErrorMessage,
|
|
5
|
+
FieldFooter as Footer,
|
|
6
|
+
FieldHeader as Header,
|
|
7
|
+
FieldIndicatorText as IndicatorText,
|
|
8
|
+
FieldLabel as Label,
|
|
9
|
+
FieldRequiredIndicator as RequiredIndicator,
|
|
10
|
+
FieldRoot as Root,
|
|
11
|
+
type FieldCharacterCountProps as CharacterCountProps,
|
|
12
|
+
type FieldDescriptionProps as DescriptionProps,
|
|
13
|
+
type FieldErrorMessageProps as ErrorMessageProps,
|
|
14
|
+
type FieldFooterProps as FooterProps,
|
|
15
|
+
type FieldHeaderProps as HeaderProps,
|
|
16
|
+
type FieldIndicatorTextProps as IndicatorTextProps,
|
|
17
|
+
type FieldLabelProps as LabelProps,
|
|
18
|
+
type FieldRootProps as RootProps,
|
|
19
|
+
} from "./Field";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { getQueriesForElement, render } from "@lynx-js/react/testing-library";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Field } from "./index";
|
|
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 getRenderedQueries() {
|
|
18
|
+
return getQueriesForElement(getRenderedRoot());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Field", () => {
|
|
22
|
+
it("renders the complete field composition with recipe slot classes", () => {
|
|
23
|
+
render(
|
|
24
|
+
<Field.Root className="custom-field" required>
|
|
25
|
+
<Field.Header>
|
|
26
|
+
<Field.Label weight="bold">
|
|
27
|
+
제목
|
|
28
|
+
<Field.RequiredIndicator />
|
|
29
|
+
<Field.IndicatorText>필수</Field.IndicatorText>
|
|
30
|
+
</Field.Label>
|
|
31
|
+
</Field.Header>
|
|
32
|
+
<Field.Footer>
|
|
33
|
+
<Field.Description>설명</Field.Description>
|
|
34
|
+
<Field.CharacterCount current={3} max={10} />
|
|
35
|
+
</Field.Footer>
|
|
36
|
+
</Field.Root>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const root = getRenderedRoot();
|
|
40
|
+
const { getByText } = getRenderedQueries();
|
|
41
|
+
const fieldRoot = root.querySelector(".seed-field__root");
|
|
42
|
+
|
|
43
|
+
expect(fieldRoot).toHaveClass("custom-field");
|
|
44
|
+
expect(root.querySelector(".seed-field__header")).toBeInTheDocument();
|
|
45
|
+
expect(root.querySelector(".seed-field__footer")).toBeInTheDocument();
|
|
46
|
+
expect(getByText("제목")).toHaveClass("seed-field-label__root--weight_bold");
|
|
47
|
+
expect(getByText("필수")).toHaveClass("seed-field-label__indicatorText");
|
|
48
|
+
expect(getByText("필수").textContent).toBe("\u00a0필수");
|
|
49
|
+
expect(getByText("*")).toHaveAttribute("accessibility-elements-hidden", "true");
|
|
50
|
+
expect(getByText("*").textContent).toBe("\u200a*");
|
|
51
|
+
expect(getByText("설명")).toHaveClass("seed-field__description");
|
|
52
|
+
expect(root.querySelector(".seed-field__characterCount")).toHaveTextContent("3");
|
|
53
|
+
expect(root.querySelector(".seed-field__maxCharacterCount")).toHaveTextContent("/10");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("applies invalid and empty character count variants", () => {
|
|
57
|
+
render(
|
|
58
|
+
<Field.Root invalid>
|
|
59
|
+
<Field.Footer>
|
|
60
|
+
<Field.ErrorMessage>오류</Field.ErrorMessage>
|
|
61
|
+
<Field.CharacterCount current={0} max={10} />
|
|
62
|
+
</Field.Footer>
|
|
63
|
+
</Field.Root>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const { getByText } = getRenderedQueries();
|
|
67
|
+
|
|
68
|
+
expect(getByText("오류")).toHaveClass("seed-field__errorMessage");
|
|
69
|
+
const root = getRenderedRoot();
|
|
70
|
+
const current = root.querySelector(".seed-field__characterCount");
|
|
71
|
+
const max = root.querySelector(".seed-field__maxCharacterCount");
|
|
72
|
+
|
|
73
|
+
expect(current).toHaveClass("seed-field__characterCount--invalid_true");
|
|
74
|
+
expect(current).toHaveClass("seed-field__characterCount--empty_true");
|
|
75
|
+
expect(max).toHaveClass("seed-field__maxCharacterCount--invalid_true");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("throws when a compound slot is rendered outside Field.Root", () => {
|
|
79
|
+
expect(() => render(<Field.Footer />)).toThrow(/ClassNamesProvider/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import * as React from "@lynx-js/react";
|
|
2
|
+
import type { NodesRef } from "@lynx-js/types";
|
|
3
|
+
import { field, type FieldVariantProps } from "@seed-design/lynx-css/recipes/field";
|
|
4
|
+
import { fieldLabel, type FieldLabelVariantProps } from "@seed-design/lynx-css/recipes/field-label";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
|
|
7
|
+
import type { LynxStyledElementProps, LynxTextRef, LynxViewRef } from "../../types";
|
|
8
|
+
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
9
|
+
import { FieldContext, useFieldContext } from "./context";
|
|
10
|
+
|
|
11
|
+
const { ClassNamesProvider: FieldClassNamesProvider, useClassNames: useFieldClassNames } =
|
|
12
|
+
createSlotRecipeContext(field);
|
|
13
|
+
const { ClassNamesProvider: FieldLabelClassNamesProvider, useClassNames: useFieldLabelClassNames } =
|
|
14
|
+
createSlotRecipeContext(fieldLabel);
|
|
15
|
+
|
|
16
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @platform Lynx
|
|
20
|
+
*
|
|
21
|
+
* 웹 대비 미지원 기능:
|
|
22
|
+
* - HTML label의 `for` 연결과 native form의 `name` 제출 모델
|
|
23
|
+
* - DOM ARIA id 연결. 입력 컴포넌트에서 `accessibility-label`을 사용해야 함
|
|
24
|
+
*/
|
|
25
|
+
export interface FieldRootProps extends Omit<FieldVariantProps, "empty">, LynxStyledElementProps {
|
|
26
|
+
required?: boolean;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
readOnly?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const FieldRoot = React.forwardRef<NodesRef, FieldRootProps>((props, forwardedRef) => {
|
|
32
|
+
const [variantProps, otherProps] = field.splitVariantProps(props);
|
|
33
|
+
const {
|
|
34
|
+
children,
|
|
35
|
+
className,
|
|
36
|
+
required = false,
|
|
37
|
+
disabled = false,
|
|
38
|
+
readOnly = false,
|
|
39
|
+
...nativeProps
|
|
40
|
+
} = otherProps;
|
|
41
|
+
const invalid = variantProps.invalid ?? false;
|
|
42
|
+
const [focused, setFocused] = React.useState(false);
|
|
43
|
+
const rootRef = React.useRef<NodesRef | null>(null);
|
|
44
|
+
const classes = field({ invalid });
|
|
45
|
+
|
|
46
|
+
const mergedRef = React.useCallback(
|
|
47
|
+
(node: NodesRef | null) => {
|
|
48
|
+
"background only";
|
|
49
|
+
|
|
50
|
+
rootRef.current = node;
|
|
51
|
+
if (typeof forwardedRef === "function") {
|
|
52
|
+
forwardedRef(node);
|
|
53
|
+
} else if (forwardedRef) {
|
|
54
|
+
forwardedRef.current = node;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[forwardedRef],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const contextValue = React.useMemo(
|
|
61
|
+
() => ({
|
|
62
|
+
rootRef,
|
|
63
|
+
disabled,
|
|
64
|
+
invalid,
|
|
65
|
+
readOnly,
|
|
66
|
+
required,
|
|
67
|
+
focused,
|
|
68
|
+
setFocused,
|
|
69
|
+
}),
|
|
70
|
+
[disabled, focused, invalid, readOnly, required],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<FieldContext.Provider value={contextValue}>
|
|
75
|
+
<FieldClassNamesProvider value={classes}>
|
|
76
|
+
<view ref={mergedRef} className={clsx(classes.root, className)} {...nativeProps}>
|
|
77
|
+
{children}
|
|
78
|
+
</view>
|
|
79
|
+
</FieldClassNamesProvider>
|
|
80
|
+
</FieldContext.Provider>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
FieldRoot.displayName = "FieldRoot";
|
|
84
|
+
|
|
85
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
86
|
+
|
|
87
|
+
export interface FieldHeaderProps extends LynxStyledElementProps {}
|
|
88
|
+
|
|
89
|
+
export const FieldHeader = React.forwardRef<unknown, FieldHeaderProps>((props, ref) => {
|
|
90
|
+
const classes = useFieldClassNames();
|
|
91
|
+
const { children, className, ...nativeProps } = props;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<view
|
|
95
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
96
|
+
className={clsx(classes.header, className)}
|
|
97
|
+
{...nativeProps}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</view>
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
FieldHeader.displayName = "FieldHeader";
|
|
104
|
+
|
|
105
|
+
export interface FieldLabelProps extends FieldLabelVariantProps, LynxStyledElementProps {}
|
|
106
|
+
|
|
107
|
+
export const FieldLabel = React.forwardRef<unknown, FieldLabelProps>((props, ref) => {
|
|
108
|
+
const [variantProps, otherProps] = fieldLabel.splitVariantProps(props);
|
|
109
|
+
const { children, className, ...nativeProps } = otherProps;
|
|
110
|
+
const classes = fieldLabel(variantProps);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<FieldLabelClassNamesProvider value={classes}>
|
|
114
|
+
<text
|
|
115
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
116
|
+
className={clsx(classes.root, className)}
|
|
117
|
+
{...nativeProps}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</text>
|
|
121
|
+
</FieldLabelClassNamesProvider>
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
FieldLabel.displayName = "FieldLabel";
|
|
125
|
+
|
|
126
|
+
export interface FieldIndicatorTextProps extends LynxStyledElementProps {}
|
|
127
|
+
|
|
128
|
+
export const FieldIndicatorText = React.forwardRef<unknown, FieldIndicatorTextProps>(
|
|
129
|
+
(props, ref) => {
|
|
130
|
+
const classes = useFieldLabelClassNames();
|
|
131
|
+
const { children, className, ...nativeProps } = props;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<text
|
|
135
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
136
|
+
className={clsx(classes.indicatorText, className)}
|
|
137
|
+
{...nativeProps}
|
|
138
|
+
>
|
|
139
|
+
{"\u00a0"}
|
|
140
|
+
{children}
|
|
141
|
+
</text>
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
FieldIndicatorText.displayName = "FieldIndicatorText";
|
|
146
|
+
|
|
147
|
+
export interface FieldRequiredIndicatorProps extends LynxStyledElementProps {}
|
|
148
|
+
|
|
149
|
+
export const FieldRequiredIndicator = React.forwardRef<unknown, FieldRequiredIndicatorProps>(
|
|
150
|
+
(props, ref) => {
|
|
151
|
+
const classes = useFieldLabelClassNames();
|
|
152
|
+
const { children = "*", className, ...nativeProps } = props;
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<text
|
|
156
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
157
|
+
accessibility-elements-hidden={true}
|
|
158
|
+
className={clsx(classes.indicatorIcon, className)}
|
|
159
|
+
{...nativeProps}
|
|
160
|
+
>
|
|
161
|
+
{"\u200a"}
|
|
162
|
+
{children}
|
|
163
|
+
</text>
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
FieldRequiredIndicator.displayName = "FieldRequiredIndicator";
|
|
168
|
+
|
|
169
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
170
|
+
|
|
171
|
+
export interface FieldFooterProps extends LynxStyledElementProps {}
|
|
172
|
+
|
|
173
|
+
export const FieldFooter = React.forwardRef<unknown, FieldFooterProps>((props, ref) => {
|
|
174
|
+
const classes = useFieldClassNames();
|
|
175
|
+
const { children, className, ...nativeProps } = props;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<view
|
|
179
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
180
|
+
className={clsx(classes.footer, className)}
|
|
181
|
+
{...nativeProps}
|
|
182
|
+
>
|
|
183
|
+
{children}
|
|
184
|
+
</view>
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
FieldFooter.displayName = "FieldFooter";
|
|
188
|
+
|
|
189
|
+
export interface FieldDescriptionProps extends LynxStyledElementProps {}
|
|
190
|
+
|
|
191
|
+
export const FieldDescription = React.forwardRef<unknown, FieldDescriptionProps>((props, ref) => {
|
|
192
|
+
const classes = useFieldClassNames();
|
|
193
|
+
const { children, className, ...nativeProps } = props;
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<text
|
|
197
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
198
|
+
className={clsx(classes.description, className)}
|
|
199
|
+
{...nativeProps}
|
|
200
|
+
>
|
|
201
|
+
{children}
|
|
202
|
+
</text>
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
FieldDescription.displayName = "FieldDescription";
|
|
206
|
+
|
|
207
|
+
export interface FieldErrorMessageProps extends LynxStyledElementProps {}
|
|
208
|
+
|
|
209
|
+
export const FieldErrorMessage = React.forwardRef<unknown, FieldErrorMessageProps>((props, ref) => {
|
|
210
|
+
const classes = useFieldClassNames();
|
|
211
|
+
const { children, className, ...nativeProps } = props;
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<text
|
|
215
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
216
|
+
className={clsx(classes.errorMessage, className)}
|
|
217
|
+
{...nativeProps}
|
|
218
|
+
>
|
|
219
|
+
{children}
|
|
220
|
+
</text>
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
FieldErrorMessage.displayName = "FieldErrorMessage";
|
|
224
|
+
|
|
225
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
226
|
+
|
|
227
|
+
export interface FieldCharacterCountProps extends LynxStyledElementProps {
|
|
228
|
+
/** 현재 문자 또는 grapheme 수 */
|
|
229
|
+
current: number;
|
|
230
|
+
/** 허용하는 최대 문자 또는 grapheme 수 */
|
|
231
|
+
max: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const FieldCharacterCount = React.forwardRef<unknown, FieldCharacterCountProps>(
|
|
235
|
+
(props, ref) => {
|
|
236
|
+
const { current, max, children: _children, className, ...nativeProps } = props;
|
|
237
|
+
const context = useFieldContext({ strict: true });
|
|
238
|
+
const classes = field({ invalid: context.invalid, empty: current === 0 });
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<view
|
|
242
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
243
|
+
className={clsx(classes.characterCountArea, className)}
|
|
244
|
+
{...nativeProps}
|
|
245
|
+
>
|
|
246
|
+
<text className={classes.characterCount}>{current}</text>
|
|
247
|
+
<text className={classes.maxCharacterCount}>/{max}</text>
|
|
248
|
+
</view>
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
FieldCharacterCount.displayName = "FieldCharacterCount";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "@lynx-js/react";
|
|
2
|
+
import type { NodesRef } from "@lynx-js/types";
|
|
3
|
+
|
|
4
|
+
export interface FieldContextValue {
|
|
5
|
+
rootRef: React.RefObject<NodesRef | null>;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
invalid: boolean;
|
|
8
|
+
readOnly: boolean;
|
|
9
|
+
required: boolean;
|
|
10
|
+
focused: boolean;
|
|
11
|
+
setFocused: (focused: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FieldContext = React.createContext<FieldContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export function useFieldContext(options: { strict: true }): FieldContextValue;
|
|
17
|
+
export function useFieldContext(options?: { strict?: false }): FieldContextValue | null;
|
|
18
|
+
export function useFieldContext(options: { strict?: boolean } = {}): FieldContextValue | null {
|
|
19
|
+
const context = React.useContext(FieldContext);
|
|
20
|
+
|
|
21
|
+
if (options.strict && !context) {
|
|
22
|
+
throw new Error("Field compound components must be rendered inside <Field.Root>.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return context;
|
|
26
|
+
}
|