@idealyst/components 1.2.127 → 1.2.128
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/package.json +4 -4
- package/src/Table/Table.native.tsx +29 -22
- package/src/Table/Table.styles.tsx +1 -0
- package/src/Table/Table.web.tsx +151 -28
- package/src/Table/types.ts +24 -0
- package/src/TextArea/TextArea.native.tsx +10 -5
- package/src/TextArea/TextArea.web.tsx +10 -5
- package/src/TextInput/TextInput.native.tsx +12 -5
- package/src/TextInput/TextInput.web.tsx +12 -5
- package/src/index.ts +1 -0
- package/src/utils/refTypes.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.128",
|
|
4
4
|
"description": "Shared component library for React and React Native",
|
|
5
5
|
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
|
|
6
6
|
"readme": "README.md",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"publish:npm": "npm publish"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@idealyst/theme": "^1.2.
|
|
59
|
+
"@idealyst/theme": "^1.2.128",
|
|
60
60
|
"@mdi/js": ">=7.0.0",
|
|
61
61
|
"@mdi/react": ">=1.0.0",
|
|
62
62
|
"@react-native-vector-icons/common": ">=12.0.0",
|
|
@@ -111,8 +111,8 @@
|
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@idealyst/blur": "^1.2.40",
|
|
114
|
-
"@idealyst/theme": "^1.2.
|
|
115
|
-
"@idealyst/tooling": "^1.2.
|
|
114
|
+
"@idealyst/theme": "^1.2.128",
|
|
115
|
+
"@idealyst/tooling": "^1.2.128",
|
|
116
116
|
"@mdi/react": "^1.6.1",
|
|
117
117
|
"@types/react": "^19.1.0",
|
|
118
118
|
"react": "^19.1.0",
|
|
@@ -187,6 +187,7 @@ function TableInner<T = any>({
|
|
|
187
187
|
size = 'md',
|
|
188
188
|
stickyHeader: _stickyHeader = false,
|
|
189
189
|
onRowPress,
|
|
190
|
+
emptyState,
|
|
190
191
|
// Spacing variants from ContainerStyleProps
|
|
191
192
|
gap,
|
|
192
193
|
padding,
|
|
@@ -284,28 +285,34 @@ function TableInner<T = any>({
|
|
|
284
285
|
|
|
285
286
|
{/* Body */}
|
|
286
287
|
<View style={tbodyStyle}>
|
|
287
|
-
{data.
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
288
|
+
{data.length === 0 && emptyState ? (
|
|
289
|
+
<View style={{ alignItems: 'center', padding: 16 }}>
|
|
290
|
+
{emptyState}
|
|
291
|
+
</View>
|
|
292
|
+
) : (
|
|
293
|
+
data.map((row, rowIndex) => (
|
|
294
|
+
<TR
|
|
295
|
+
key={rowIndex}
|
|
296
|
+
size={size}
|
|
297
|
+
type={type}
|
|
298
|
+
clickable={isClickable}
|
|
299
|
+
onPress={() => onRowPress?.(row, rowIndex)}
|
|
300
|
+
testID={testID ? `${testID}-row-${rowIndex}` : undefined}
|
|
301
|
+
>
|
|
302
|
+
{columns.map((column) => (
|
|
303
|
+
<TD
|
|
304
|
+
key={column.key}
|
|
305
|
+
size={size}
|
|
306
|
+
type={type}
|
|
307
|
+
align={column.align}
|
|
308
|
+
width={column.width}
|
|
309
|
+
>
|
|
310
|
+
{getCellValue(column, row, rowIndex)}
|
|
311
|
+
</TD>
|
|
312
|
+
))}
|
|
313
|
+
</TR>
|
|
314
|
+
))
|
|
315
|
+
)}
|
|
309
316
|
</View>
|
|
310
317
|
|
|
311
318
|
{/* Footer */}
|
|
@@ -176,6 +176,7 @@ export const tableStyles = defineStyle('Table', (theme: Theme) => ({
|
|
|
176
176
|
},
|
|
177
177
|
},
|
|
178
178
|
_web: {
|
|
179
|
+
position: 'relative',
|
|
179
180
|
borderBottom: `2px solid ${theme.colors.border.primary}`,
|
|
180
181
|
borderRight: type === 'bordered' ? `1px solid ${theme.colors.border.primary}` : undefined,
|
|
181
182
|
':last-child': type === 'bordered' ? { borderRight: 'none' } : {},
|
package/src/Table/Table.web.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, ReactNode } from 'react';
|
|
1
|
+
import { useMemo, useRef, useCallback, ReactNode } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { tableStyles } from './Table.styles';
|
|
4
4
|
import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant } from './types';
|
|
@@ -23,6 +23,11 @@ interface THProps {
|
|
|
23
23
|
type?: TableType;
|
|
24
24
|
align?: TableAlignVariant;
|
|
25
25
|
width?: number | string;
|
|
26
|
+
sticky?: boolean;
|
|
27
|
+
stickyLeft?: number | string;
|
|
28
|
+
resizable?: boolean;
|
|
29
|
+
onResize?: (width: number) => void;
|
|
30
|
+
minWidth?: number;
|
|
26
31
|
accessibilitySort?: 'ascending' | 'descending' | 'none' | 'other';
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -32,6 +37,8 @@ interface TDProps {
|
|
|
32
37
|
type?: TableType;
|
|
33
38
|
align?: TableAlignVariant;
|
|
34
39
|
width?: number | string;
|
|
40
|
+
sticky?: boolean;
|
|
41
|
+
stickyLeft?: number | string;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
// ============================================================================
|
|
@@ -75,6 +82,11 @@ function TH({
|
|
|
75
82
|
type = 'standard',
|
|
76
83
|
align = 'left',
|
|
77
84
|
width,
|
|
85
|
+
sticky,
|
|
86
|
+
stickyLeft,
|
|
87
|
+
resizable,
|
|
88
|
+
onResize,
|
|
89
|
+
minWidth = 50,
|
|
78
90
|
accessibilitySort,
|
|
79
91
|
}: THProps) {
|
|
80
92
|
tableStyles.useVariants({
|
|
@@ -84,15 +96,69 @@ function TH({
|
|
|
84
96
|
});
|
|
85
97
|
|
|
86
98
|
const headerCellProps = getWebProps([(tableStyles.headerCell as any)({})]);
|
|
99
|
+
const thRef = useRef<HTMLTableCellElement>(null);
|
|
100
|
+
|
|
101
|
+
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
e.stopPropagation();
|
|
104
|
+
const th = thRef.current;
|
|
105
|
+
if (!th) return;
|
|
106
|
+
|
|
107
|
+
const startX = e.clientX;
|
|
108
|
+
const startWidth = th.getBoundingClientRect().width;
|
|
109
|
+
|
|
110
|
+
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
111
|
+
const newWidth = Math.max(minWidth, startWidth + (moveEvent.clientX - startX));
|
|
112
|
+
th.style.width = `${newWidth}px`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handlePointerUp = (_upEvent: PointerEvent) => {
|
|
116
|
+
document.removeEventListener('pointermove', handlePointerMove);
|
|
117
|
+
document.removeEventListener('pointerup', handlePointerUp);
|
|
118
|
+
const finalWidth = th.getBoundingClientRect().width;
|
|
119
|
+
onResize?.(finalWidth);
|
|
120
|
+
// Remove inline cursor override
|
|
121
|
+
document.body.style.cursor = '';
|
|
122
|
+
document.body.style.userSelect = '';
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Prevent text selection and set resize cursor globally during drag
|
|
126
|
+
document.body.style.cursor = 'col-resize';
|
|
127
|
+
document.body.style.userSelect = 'none';
|
|
128
|
+
document.addEventListener('pointermove', handlePointerMove);
|
|
129
|
+
document.addEventListener('pointerup', handlePointerUp);
|
|
130
|
+
}, [minWidth, onResize]);
|
|
131
|
+
|
|
132
|
+
const stickyStyle: React.CSSProperties | undefined = sticky ? {
|
|
133
|
+
position: 'sticky',
|
|
134
|
+
left: stickyLeft ?? 0,
|
|
135
|
+
zIndex: 11,
|
|
136
|
+
backgroundColor: 'inherit',
|
|
137
|
+
} : undefined;
|
|
87
138
|
|
|
88
139
|
return (
|
|
89
140
|
<th
|
|
90
141
|
{...headerCellProps}
|
|
142
|
+
ref={thRef}
|
|
91
143
|
scope="col"
|
|
92
144
|
aria-sort={accessibilitySort}
|
|
93
|
-
style={{ width }}
|
|
145
|
+
style={{ width, ...stickyStyle }}
|
|
94
146
|
>
|
|
95
147
|
{children}
|
|
148
|
+
{resizable && (
|
|
149
|
+
<span
|
|
150
|
+
onPointerDown={handlePointerDown}
|
|
151
|
+
style={{
|
|
152
|
+
position: 'absolute',
|
|
153
|
+
right: 0,
|
|
154
|
+
top: 0,
|
|
155
|
+
bottom: 0,
|
|
156
|
+
width: 4,
|
|
157
|
+
cursor: 'col-resize',
|
|
158
|
+
userSelect: 'none',
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
96
162
|
</th>
|
|
97
163
|
);
|
|
98
164
|
}
|
|
@@ -107,6 +173,8 @@ function TD({
|
|
|
107
173
|
type = 'standard',
|
|
108
174
|
align = 'left',
|
|
109
175
|
width,
|
|
176
|
+
sticky,
|
|
177
|
+
stickyLeft,
|
|
110
178
|
}: TDProps) {
|
|
111
179
|
tableStyles.useVariants({
|
|
112
180
|
size,
|
|
@@ -116,10 +184,17 @@ function TD({
|
|
|
116
184
|
|
|
117
185
|
const cellProps = getWebProps([(tableStyles.cell as any)({})]);
|
|
118
186
|
|
|
187
|
+
const stickyStyle: React.CSSProperties | undefined = sticky ? {
|
|
188
|
+
position: 'sticky',
|
|
189
|
+
left: stickyLeft ?? 0,
|
|
190
|
+
zIndex: 1,
|
|
191
|
+
backgroundColor: 'inherit',
|
|
192
|
+
} : undefined;
|
|
193
|
+
|
|
119
194
|
return (
|
|
120
195
|
<td
|
|
121
196
|
{...cellProps}
|
|
122
|
-
style={{ width }}
|
|
197
|
+
style={{ width, ...stickyStyle }}
|
|
123
198
|
>
|
|
124
199
|
{children}
|
|
125
200
|
</td>
|
|
@@ -136,6 +211,8 @@ interface TFProps {
|
|
|
136
211
|
type?: TableType;
|
|
137
212
|
align?: TableAlignVariant;
|
|
138
213
|
width?: number | string;
|
|
214
|
+
sticky?: boolean;
|
|
215
|
+
stickyLeft?: number | string;
|
|
139
216
|
}
|
|
140
217
|
|
|
141
218
|
function TF({
|
|
@@ -144,6 +221,8 @@ function TF({
|
|
|
144
221
|
type = 'standard',
|
|
145
222
|
align = 'left',
|
|
146
223
|
width,
|
|
224
|
+
sticky,
|
|
225
|
+
stickyLeft,
|
|
147
226
|
}: TFProps) {
|
|
148
227
|
tableStyles.useVariants({
|
|
149
228
|
size,
|
|
@@ -153,10 +232,17 @@ function TF({
|
|
|
153
232
|
|
|
154
233
|
const footerCellProps = getWebProps([(tableStyles.footerCell as any)({})]);
|
|
155
234
|
|
|
235
|
+
const stickyStyle: React.CSSProperties | undefined = sticky ? {
|
|
236
|
+
position: 'sticky',
|
|
237
|
+
left: stickyLeft ?? 0,
|
|
238
|
+
zIndex: 1,
|
|
239
|
+
backgroundColor: 'inherit',
|
|
240
|
+
} : undefined;
|
|
241
|
+
|
|
156
242
|
return (
|
|
157
243
|
<td
|
|
158
244
|
{...footerCellProps}
|
|
159
|
-
style={{ width }}
|
|
245
|
+
style={{ width, ...stickyStyle }}
|
|
160
246
|
>
|
|
161
247
|
{children}
|
|
162
248
|
</td>
|
|
@@ -176,8 +262,10 @@ function Table<T = any>({
|
|
|
176
262
|
data,
|
|
177
263
|
type = 'standard',
|
|
178
264
|
size = 'md',
|
|
179
|
-
stickyHeader
|
|
265
|
+
stickyHeader = false,
|
|
180
266
|
onRowPress,
|
|
267
|
+
onColumnResize,
|
|
268
|
+
emptyState,
|
|
181
269
|
// Spacing variants from ContainerStyleProps
|
|
182
270
|
gap,
|
|
183
271
|
padding,
|
|
@@ -241,10 +329,28 @@ function Table<T = any>({
|
|
|
241
329
|
return column.footer;
|
|
242
330
|
};
|
|
243
331
|
|
|
332
|
+
// Compute cumulative left offsets for sticky columns
|
|
333
|
+
const stickyLeftMap = useMemo(() => {
|
|
334
|
+
const map = new Map<string, number | string>();
|
|
335
|
+
let cumulativeLeft = 0;
|
|
336
|
+
for (const col of columns) {
|
|
337
|
+
if (!col.sticky) continue;
|
|
338
|
+
map.set(col.key, cumulativeLeft);
|
|
339
|
+
if (typeof col.width === 'number') {
|
|
340
|
+
cumulativeLeft += col.width;
|
|
341
|
+
} else if (typeof col.width === 'string') {
|
|
342
|
+
// For string widths (e.g. '200px'), use as-is for the first, then can't accumulate
|
|
343
|
+
// Only numeric widths support proper multi-column stacking
|
|
344
|
+
map.set(col.key, cumulativeLeft > 0 ? cumulativeLeft : 0);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return map;
|
|
348
|
+
}, [columns]);
|
|
349
|
+
|
|
244
350
|
return (
|
|
245
351
|
<div {...containerProps} {...ariaProps} id={id} data-testid={testID}>
|
|
246
352
|
<table {...tableProps} role="table">
|
|
247
|
-
<thead {...getWebProps([(tableStyles.thead as any)({})])}>
|
|
353
|
+
<thead {...getWebProps([(tableStyles.thead as any)({ sticky: stickyHeader })])}>
|
|
248
354
|
<tr>
|
|
249
355
|
{columns.map((column) => (
|
|
250
356
|
<TH
|
|
@@ -253,6 +359,11 @@ function Table<T = any>({
|
|
|
253
359
|
type={type}
|
|
254
360
|
align={column.align}
|
|
255
361
|
width={column.width}
|
|
362
|
+
sticky={column.sticky}
|
|
363
|
+
stickyLeft={stickyLeftMap.get(column.key)}
|
|
364
|
+
resizable={column.resizable}
|
|
365
|
+
minWidth={column.minWidth}
|
|
366
|
+
onResize={onColumnResize ? (w) => onColumnResize(column.key, w) : undefined}
|
|
256
367
|
accessibilitySort={column.accessibilitySort}
|
|
257
368
|
>
|
|
258
369
|
{column.title}
|
|
@@ -261,28 +372,38 @@ function Table<T = any>({
|
|
|
261
372
|
</tr>
|
|
262
373
|
</thead>
|
|
263
374
|
<tbody {...getWebProps([(tableStyles.tbody as any)({})])}>
|
|
264
|
-
{data.
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
375
|
+
{data.length === 0 && emptyState ? (
|
|
376
|
+
<tr>
|
|
377
|
+
<td colSpan={columns.length} style={{ textAlign: 'center' }}>
|
|
378
|
+
{emptyState}
|
|
379
|
+
</td>
|
|
380
|
+
</tr>
|
|
381
|
+
) : (
|
|
382
|
+
data.map((row, rowIndex) => (
|
|
383
|
+
<TR
|
|
384
|
+
key={rowIndex}
|
|
385
|
+
size={size}
|
|
386
|
+
type={type}
|
|
387
|
+
clickable={isClickable}
|
|
388
|
+
onClick={() => onRowPress?.(row, rowIndex)}
|
|
389
|
+
testID={testID ? `${testID}-row-${rowIndex}` : undefined}
|
|
390
|
+
>
|
|
391
|
+
{columns.map((column) => (
|
|
392
|
+
<TD
|
|
393
|
+
key={column.key}
|
|
394
|
+
size={size}
|
|
395
|
+
type={type}
|
|
396
|
+
align={column.align}
|
|
397
|
+
width={column.width}
|
|
398
|
+
sticky={column.sticky}
|
|
399
|
+
stickyLeft={stickyLeftMap.get(column.key)}
|
|
400
|
+
>
|
|
401
|
+
{getCellValue(column, row, rowIndex)}
|
|
402
|
+
</TD>
|
|
403
|
+
))}
|
|
404
|
+
</TR>
|
|
405
|
+
))
|
|
406
|
+
)}
|
|
286
407
|
</tbody>
|
|
287
408
|
{hasFooter && (
|
|
288
409
|
<tfoot {...getWebProps([(tableStyles.tfoot as any)({})])}>
|
|
@@ -294,6 +415,8 @@ function Table<T = any>({
|
|
|
294
415
|
type={type}
|
|
295
416
|
align={column.align}
|
|
296
417
|
width={column.width}
|
|
418
|
+
sticky={column.sticky}
|
|
419
|
+
stickyLeft={stickyLeftMap.get(column.key)}
|
|
297
420
|
>
|
|
298
421
|
{getFooterContent(column) ?? null}
|
|
299
422
|
</TF>
|
package/src/Table/types.ts
CHANGED
|
@@ -17,6 +17,20 @@ export interface TableColumn<T = any> extends SortableAccessibilityProps {
|
|
|
17
17
|
footer?: ReactNode | ((data: T[]) => ReactNode);
|
|
18
18
|
width?: number | string;
|
|
19
19
|
align?: TableAlignVariant;
|
|
20
|
+
/**
|
|
21
|
+
* Makes this column sticky (pinned) when scrolling horizontally.
|
|
22
|
+
* Web only — uses CSS `position: sticky`.
|
|
23
|
+
*/
|
|
24
|
+
sticky?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Allows the column to be resized by dragging the right edge of the header.
|
|
27
|
+
* Web only.
|
|
28
|
+
*/
|
|
29
|
+
resizable?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Minimum width when resizing (default: 50).
|
|
32
|
+
*/
|
|
33
|
+
minWidth?: number;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
/**
|
|
@@ -33,6 +47,16 @@ export interface TableProps<T = any> extends ContainerStyleProps, AccessibilityP
|
|
|
33
47
|
size?: TableSizeVariant;
|
|
34
48
|
stickyHeader?: boolean;
|
|
35
49
|
onRowPress?: (row: T, index: number) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Called when a column is resized via drag handle.
|
|
52
|
+
* Receives the column key and the new width in pixels.
|
|
53
|
+
*/
|
|
54
|
+
onColumnResize?: (key: string, width: number) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Content to display when `data` is empty.
|
|
57
|
+
* Renders in place of the table body.
|
|
58
|
+
*/
|
|
59
|
+
emptyState?: ReactNode;
|
|
36
60
|
style?: StyleProp<ViewStyle>;
|
|
37
61
|
testID?: string;
|
|
38
62
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { useState, forwardRef, useMemo, useEffect, useRef } from 'react';
|
|
1
|
+
import { useState, forwardRef, useMemo, useEffect, useRef, useImperativeHandle } from 'react';
|
|
2
2
|
import { View, TextInput, NativeSyntheticEvent, TextInputContentSizeChangeEventData, TextInput as RNTextInput } from 'react-native';
|
|
3
3
|
import { textAreaStyles } from './TextArea.styles';
|
|
4
4
|
import Text from '../Text';
|
|
5
5
|
import type { TextAreaProps } from './types';
|
|
6
6
|
import { getNativeFormAccessibilityProps } from '../utils/accessibility';
|
|
7
|
-
import type {
|
|
8
|
-
import useMergeRefs from '../hooks/useMergeRefs';
|
|
7
|
+
import type { TextInputHandle } from '../utils/refTypes';
|
|
9
8
|
|
|
10
|
-
const TextArea = forwardRef<
|
|
9
|
+
const TextArea = forwardRef<TextInputHandle, TextAreaProps>(({
|
|
11
10
|
value: controlledValue,
|
|
12
11
|
defaultValue = '',
|
|
13
12
|
onChange,
|
|
@@ -52,6 +51,12 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
|
|
|
52
51
|
const [contentHeight, setContentHeight] = useState<number | undefined>(undefined);
|
|
53
52
|
const textInputRef = useRef<RNTextInput>(null);
|
|
54
53
|
|
|
54
|
+
// Expose focus/blur via imperative handle
|
|
55
|
+
useImperativeHandle(ref, () => ({
|
|
56
|
+
focus: () => textInputRef.current?.focus(),
|
|
57
|
+
blur: () => textInputRef.current?.blur(),
|
|
58
|
+
}), []);
|
|
59
|
+
|
|
55
60
|
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
56
61
|
|
|
57
62
|
// When value is cleared externally, trigger a layout recalculation via setNativeProps
|
|
@@ -175,7 +180,7 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
|
|
|
175
180
|
|
|
176
181
|
<View style={[textareaContainerStyleComputed, fill && { flex: 1 }]}>
|
|
177
182
|
<TextInput
|
|
178
|
-
ref={
|
|
183
|
+
ref={textInputRef}
|
|
179
184
|
{...nativeA11yProps}
|
|
180
185
|
style={[
|
|
181
186
|
textareaStyleComputed,
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, forwardRef, useMemo, useCallback } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, forwardRef, useMemo, useCallback, useImperativeHandle } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { textAreaStyles } from './TextArea.styles';
|
|
4
4
|
import type { TextAreaProps } from './types';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
6
|
import { getWebFormAriaProps, combineIds, generateAccessibilityId } from '../utils/accessibility';
|
|
7
|
-
import type {
|
|
7
|
+
import type { TextInputHandle } from '../utils/refTypes';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Multi-line text input with auto-grow, character counting, and validation support.
|
|
11
11
|
* Includes label, helper text, and error message display.
|
|
12
12
|
*/
|
|
13
|
-
const TextArea = forwardRef<
|
|
13
|
+
const TextArea = forwardRef<TextInputHandle, TextAreaProps>(({
|
|
14
14
|
value: controlledValue,
|
|
15
15
|
defaultValue = '',
|
|
16
16
|
onChange,
|
|
@@ -61,6 +61,12 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
|
|
|
61
61
|
const [isFocused, setIsFocused] = useState(false);
|
|
62
62
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
63
63
|
|
|
64
|
+
// Expose focus/blur via imperative handle
|
|
65
|
+
useImperativeHandle(ref, () => ({
|
|
66
|
+
focus: () => textareaRef.current?.focus(),
|
|
67
|
+
blur: () => textareaRef.current?.blur(),
|
|
68
|
+
}), []);
|
|
69
|
+
|
|
64
70
|
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
65
71
|
const hasError = Boolean(error);
|
|
66
72
|
|
|
@@ -240,11 +246,10 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
|
|
|
240
246
|
overflowStyle,
|
|
241
247
|
].filter(Boolean));
|
|
242
248
|
|
|
243
|
-
const mergedRef = useMergeRefs(ref, containerProps.ref);
|
|
244
249
|
const mergedTextareaRef = useMergeRefs(textareaRef, computedTextareaProps.ref);
|
|
245
250
|
|
|
246
251
|
return (
|
|
247
|
-
<div {...containerProps}
|
|
252
|
+
<div {...containerProps} id={id} data-testid={testID} style={{ ...(containerProps as any).style, ...(fill ? { flex: 1, display: 'flex', flexDirection: 'column' } : {}) }}>
|
|
248
253
|
{label && (
|
|
249
254
|
<label {...labelProps} id={labelId} htmlFor={textareaId}>{label}</label>
|
|
250
255
|
)}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import React, { useState, isValidElement, useMemo, useEffect, useRef, useCallback } from 'react';
|
|
1
|
+
import React, { useState, isValidElement, useMemo, useEffect, useRef, useCallback, useImperativeHandle } from 'react';
|
|
2
2
|
import { View, TextInput as RNTextInput, TouchableOpacity, Platform, TextInputProps as RNTextInputProps } from 'react-native';
|
|
3
3
|
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
|
|
4
4
|
import { useUnistyles } from 'react-native-unistyles';
|
|
5
5
|
import { TextInputProps } from './types';
|
|
6
6
|
import { textInputStyles } from './TextInput.styles';
|
|
7
7
|
import { getNativeFormAccessibilityProps } from '../utils/accessibility';
|
|
8
|
-
import type {
|
|
8
|
+
import type { TextInputHandle } from '../utils/refTypes';
|
|
9
9
|
import Text from '../Text';
|
|
10
10
|
|
|
11
11
|
// Inner TextInput component that can be memoized to prevent re-renders
|
|
@@ -52,7 +52,7 @@ const InnerRNTextInput = React.memo<InnerTextInputProps>(
|
|
|
52
52
|
}
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
const TextInput = React.forwardRef<
|
|
55
|
+
const TextInput = React.forwardRef<TextInputHandle, TextInputProps>(({
|
|
56
56
|
value,
|
|
57
57
|
onChangeText,
|
|
58
58
|
onFocus,
|
|
@@ -94,6 +94,13 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
|
|
|
94
94
|
}, ref) => {
|
|
95
95
|
const [isFocused, setIsFocused] = useState(false);
|
|
96
96
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
|
97
|
+
const inputRef = useRef<RNTextInput>(null);
|
|
98
|
+
|
|
99
|
+
// Expose focus/blur via imperative handle
|
|
100
|
+
useImperativeHandle(ref, () => ({
|
|
101
|
+
focus: () => inputRef.current?.focus(),
|
|
102
|
+
blur: () => inputRef.current?.blur(),
|
|
103
|
+
}), []);
|
|
97
104
|
|
|
98
105
|
// Derive hasError from error prop or hasError boolean
|
|
99
106
|
const computedHasError = Boolean(error) || hasError;
|
|
@@ -311,7 +318,7 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
|
|
|
311
318
|
|
|
312
319
|
{/* Input */}
|
|
313
320
|
<InnerRNTextInput
|
|
314
|
-
inputRef={
|
|
321
|
+
inputRef={inputRef}
|
|
315
322
|
value={value}
|
|
316
323
|
onChangeText={handleChangeText}
|
|
317
324
|
isAndroidSecure={needsAndroidSecureWorkaround}
|
|
@@ -359,7 +366,7 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
|
|
|
359
366
|
|
|
360
367
|
{/* Input */}
|
|
361
368
|
<InnerRNTextInput
|
|
362
|
-
inputRef={
|
|
369
|
+
inputRef={inputRef}
|
|
363
370
|
value={value}
|
|
364
371
|
onChangeText={handleChangeText}
|
|
365
372
|
isAndroidSecure={needsAndroidSecureWorkaround}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { isValidElement, useState, useMemo, useRef } from 'react';
|
|
1
|
+
import React, { isValidElement, useState, useMemo, useRef, useImperativeHandle } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { useUnistyles } from 'react-native-unistyles';
|
|
4
4
|
import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
|
|
@@ -7,14 +7,14 @@ import useMergeRefs from '../hooks/useMergeRefs';
|
|
|
7
7
|
import { textInputStyles } from './TextInput.styles';
|
|
8
8
|
import { TextInputProps } from './types';
|
|
9
9
|
import { getWebFormAriaProps, combineIds, generateAccessibilityId } from '../utils/accessibility';
|
|
10
|
-
import type {
|
|
10
|
+
import type { TextInputHandle } from '../utils/refTypes';
|
|
11
11
|
import { flattenStyle } from '../utils/flattenStyle';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Single-line text input field with support for icons, password visibility toggle, and validation states.
|
|
15
15
|
* Available in outlined and filled variants with multiple sizes.
|
|
16
16
|
*/
|
|
17
|
-
const TextInput = React.forwardRef<
|
|
17
|
+
const TextInput = React.forwardRef<TextInputHandle, TextInputProps>(({
|
|
18
18
|
value,
|
|
19
19
|
onChangeText,
|
|
20
20
|
onFocus,
|
|
@@ -234,8 +234,15 @@ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
|
|
|
234
234
|
accessibilityAutoComplete,
|
|
235
235
|
]);
|
|
236
236
|
|
|
237
|
-
//
|
|
238
|
-
const
|
|
237
|
+
// Internal ref for the actual <input> element
|
|
238
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
239
|
+
const mergedInputRef = useMergeRefs(inputRef, inputWebProps.ref);
|
|
240
|
+
|
|
241
|
+
// Expose focus/blur via imperative handle
|
|
242
|
+
useImperativeHandle(ref, () => ({
|
|
243
|
+
focus: () => inputRef.current?.focus(),
|
|
244
|
+
blur: () => inputRef.current?.blur(),
|
|
245
|
+
}), []);
|
|
239
246
|
|
|
240
247
|
// Helper to render left icon
|
|
241
248
|
const renderLeftIcon = () => {
|
package/src/index.ts
CHANGED
package/src/utils/refTypes.ts
CHANGED
|
@@ -44,6 +44,24 @@ export type CrossPlatformElement = IdealystElement;
|
|
|
44
44
|
*/
|
|
45
45
|
export type CrossPlatformRef = React.RefObject<IdealystElement>;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Imperative handle for text input components (TextInput, TextArea).
|
|
49
|
+
* Provides cross-platform focus/blur control via refs.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* const inputRef = React.useRef<TextInputHandle>(null);
|
|
54
|
+
* inputRef.current?.focus();
|
|
55
|
+
* inputRef.current?.blur();
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export interface TextInputHandle {
|
|
59
|
+
/** Programmatically focus the input */
|
|
60
|
+
focus: () => void;
|
|
61
|
+
/** Programmatically blur the input */
|
|
62
|
+
blur: () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
// Legacy exports kept for backwards compatibility
|
|
48
66
|
export type WebElement = IdealystElement;
|
|
49
67
|
export type NativeElement = IdealystElement;
|