@idealyst/components 1.2.127 → 1.2.129
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 +6 -6
- package/src/Table/Table.native.tsx +84 -53
- package/src/Table/Table.styles.tsx +1 -0
- package/src/Table/Table.web.tsx +160 -28
- package/src/Table/types.ts +25 -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.129",
|
|
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.129",
|
|
60
60
|
"@mdi/js": ">=7.0.0",
|
|
61
61
|
"@mdi/react": ">=1.0.0",
|
|
62
62
|
"@react-native-vector-icons/common": ">=12.0.0",
|
|
@@ -81,10 +81,10 @@
|
|
|
81
81
|
"@mdi/react": {
|
|
82
82
|
"optional": true
|
|
83
83
|
},
|
|
84
|
-
"@react-
|
|
84
|
+
"@react-native/normalize-colors": {
|
|
85
85
|
"optional": true
|
|
86
86
|
},
|
|
87
|
-
"@react-
|
|
87
|
+
"@react-navigation/bottom-tabs": {
|
|
88
88
|
"optional": true
|
|
89
89
|
},
|
|
90
90
|
"react-native": {
|
|
@@ -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.129",
|
|
115
|
+
"@idealyst/tooling": "^1.2.129",
|
|
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,
|
|
@@ -255,36 +256,34 @@ function TableInner<T = any>({
|
|
|
255
256
|
return column.footer;
|
|
256
257
|
};
|
|
257
258
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
>
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<View style={
|
|
270
|
-
|
|
271
|
-
{
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
type={type}
|
|
276
|
-
align={column.align}
|
|
277
|
-
width={column.width}
|
|
278
|
-
>
|
|
279
|
-
{column.title}
|
|
280
|
-
</TH>
|
|
281
|
-
))}
|
|
282
|
-
</View>
|
|
259
|
+
// Split columns into sticky left, scrollable, and sticky right
|
|
260
|
+
const leftCols = useMemo(() => columns.filter((c) => c.sticky === true || c.sticky === 'left'), [columns]);
|
|
261
|
+
const rightCols = useMemo(() => columns.filter((c) => c.sticky === 'right'), [columns]);
|
|
262
|
+
const scrollCols = useMemo(() => columns.filter((c) => !c.sticky), [columns]);
|
|
263
|
+
const hasStickyColumns = leftCols.length > 0 || rightCols.length > 0;
|
|
264
|
+
|
|
265
|
+
// Renders a column group (header + body + footer) for a set of columns
|
|
266
|
+
const renderColumnGroup = (cols: TableColumn<T>[]) => (
|
|
267
|
+
<View>
|
|
268
|
+
{/* Header */}
|
|
269
|
+
<View style={theadStyle}>
|
|
270
|
+
<View style={{ flexDirection: 'row' }}>
|
|
271
|
+
{cols.map((column) => (
|
|
272
|
+
<TH key={column.key} size={size} type={type} align={column.align} width={column.width}>
|
|
273
|
+
{column.title}
|
|
274
|
+
</TH>
|
|
275
|
+
))}
|
|
283
276
|
</View>
|
|
277
|
+
</View>
|
|
284
278
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
279
|
+
{/* Body */}
|
|
280
|
+
<View style={tbodyStyle}>
|
|
281
|
+
{data.length === 0 && emptyState ? (
|
|
282
|
+
<View style={{ alignItems: 'center', padding: 16 }}>
|
|
283
|
+
{emptyState}
|
|
284
|
+
</View>
|
|
285
|
+
) : (
|
|
286
|
+
data.map((row, rowIndex) => (
|
|
288
287
|
<TR
|
|
289
288
|
key={rowIndex}
|
|
290
289
|
size={size}
|
|
@@ -293,39 +292,71 @@ function TableInner<T = any>({
|
|
|
293
292
|
onPress={() => onRowPress?.(row, rowIndex)}
|
|
294
293
|
testID={testID ? `${testID}-row-${rowIndex}` : undefined}
|
|
295
294
|
>
|
|
296
|
-
{
|
|
297
|
-
<TD
|
|
298
|
-
key={column.key}
|
|
299
|
-
size={size}
|
|
300
|
-
type={type}
|
|
301
|
-
align={column.align}
|
|
302
|
-
width={column.width}
|
|
303
|
-
>
|
|
295
|
+
{cols.map((column) => (
|
|
296
|
+
<TD key={column.key} size={size} type={type} align={column.align} width={column.width}>
|
|
304
297
|
{getCellValue(column, row, rowIndex)}
|
|
305
298
|
</TD>
|
|
306
299
|
))}
|
|
307
300
|
</TR>
|
|
308
|
-
))
|
|
301
|
+
))
|
|
302
|
+
)}
|
|
303
|
+
</View>
|
|
304
|
+
|
|
305
|
+
{/* Footer */}
|
|
306
|
+
{hasFooter && (
|
|
307
|
+
<View style={(tableStyles.tfoot as any)({})}>
|
|
308
|
+
<View style={{ flexDirection: 'row' }}>
|
|
309
|
+
{cols.map((column) => (
|
|
310
|
+
<TF key={column.key} size={size} type={type} align={column.align} width={column.width}>
|
|
311
|
+
{getFooterContent(column) ?? null}
|
|
312
|
+
</TF>
|
|
313
|
+
))}
|
|
314
|
+
</View>
|
|
309
315
|
</View>
|
|
316
|
+
)}
|
|
317
|
+
</View>
|
|
318
|
+
);
|
|
310
319
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
{getFooterContent(column) ?? null}
|
|
324
|
-
</TF>
|
|
325
|
-
))}
|
|
326
|
-
</View>
|
|
320
|
+
// When there are sticky columns, render as: [left sticky] [scrollable] [right sticky]
|
|
321
|
+
if (hasStickyColumns) {
|
|
322
|
+
return (
|
|
323
|
+
<View
|
|
324
|
+
nativeID={id}
|
|
325
|
+
style={[containerStyle, { flexDirection: 'row' }, style]}
|
|
326
|
+
testID={testID}
|
|
327
|
+
{...nativeA11yProps}
|
|
328
|
+
>
|
|
329
|
+
{leftCols.length > 0 && (
|
|
330
|
+
<View style={{ zIndex: 1, backgroundColor: theadStyle.backgroundColor || containerStyle.backgroundColor }}>
|
|
331
|
+
{renderColumnGroup(leftCols)}
|
|
327
332
|
</View>
|
|
328
333
|
)}
|
|
334
|
+
<ScrollView ref={ref} horizontal style={{ flex: 1 }}>
|
|
335
|
+
<View style={tableStyle}>
|
|
336
|
+
{renderColumnGroup(scrollCols)}
|
|
337
|
+
</View>
|
|
338
|
+
</ScrollView>
|
|
339
|
+
{rightCols.length > 0 && (
|
|
340
|
+
<View style={{ zIndex: 1, backgroundColor: theadStyle.backgroundColor || containerStyle.backgroundColor }}>
|
|
341
|
+
{renderColumnGroup(rightCols)}
|
|
342
|
+
</View>
|
|
343
|
+
)}
|
|
344
|
+
</View>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// No sticky columns — original simple layout
|
|
349
|
+
return (
|
|
350
|
+
<ScrollView
|
|
351
|
+
ref={ref}
|
|
352
|
+
nativeID={id}
|
|
353
|
+
horizontal
|
|
354
|
+
style={[containerStyle, style]}
|
|
355
|
+
testID={testID}
|
|
356
|
+
{...nativeA11yProps}
|
|
357
|
+
>
|
|
358
|
+
<View style={tableStyle}>
|
|
359
|
+
{renderColumnGroup(columns)}
|
|
329
360
|
</View>
|
|
330
361
|
</ScrollView>
|
|
331
362
|
);
|
|
@@ -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,9 +1,28 @@
|
|
|
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';
|
|
5
5
|
import { getWebAriaProps } from '../utils/accessibility';
|
|
6
6
|
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Helpers
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
function getStickyStyle(
|
|
12
|
+
sticky: boolean | 'left' | 'right' | undefined,
|
|
13
|
+
offset: number | string | undefined,
|
|
14
|
+
zIndex: number,
|
|
15
|
+
): React.CSSProperties | undefined {
|
|
16
|
+
if (!sticky) return undefined;
|
|
17
|
+
const side = sticky === 'right' ? 'right' : 'left';
|
|
18
|
+
return {
|
|
19
|
+
position: 'sticky',
|
|
20
|
+
[side]: offset ?? 0,
|
|
21
|
+
zIndex,
|
|
22
|
+
backgroundColor: 'inherit',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
// ============================================================================
|
|
8
27
|
// Sub-component Props
|
|
9
28
|
// ============================================================================
|
|
@@ -23,6 +42,11 @@ interface THProps {
|
|
|
23
42
|
type?: TableType;
|
|
24
43
|
align?: TableAlignVariant;
|
|
25
44
|
width?: number | string;
|
|
45
|
+
sticky?: boolean | 'left' | 'right';
|
|
46
|
+
stickyOffset?: number | string;
|
|
47
|
+
resizable?: boolean;
|
|
48
|
+
onResize?: (width: number) => void;
|
|
49
|
+
minWidth?: number;
|
|
26
50
|
accessibilitySort?: 'ascending' | 'descending' | 'none' | 'other';
|
|
27
51
|
}
|
|
28
52
|
|
|
@@ -32,6 +56,8 @@ interface TDProps {
|
|
|
32
56
|
type?: TableType;
|
|
33
57
|
align?: TableAlignVariant;
|
|
34
58
|
width?: number | string;
|
|
59
|
+
sticky?: boolean | 'left' | 'right';
|
|
60
|
+
stickyOffset?: number | string;
|
|
35
61
|
}
|
|
36
62
|
|
|
37
63
|
// ============================================================================
|
|
@@ -75,6 +101,11 @@ function TH({
|
|
|
75
101
|
type = 'standard',
|
|
76
102
|
align = 'left',
|
|
77
103
|
width,
|
|
104
|
+
sticky,
|
|
105
|
+
stickyOffset,
|
|
106
|
+
resizable,
|
|
107
|
+
onResize,
|
|
108
|
+
minWidth = 50,
|
|
78
109
|
accessibilitySort,
|
|
79
110
|
}: THProps) {
|
|
80
111
|
tableStyles.useVariants({
|
|
@@ -84,15 +115,62 @@ function TH({
|
|
|
84
115
|
});
|
|
85
116
|
|
|
86
117
|
const headerCellProps = getWebProps([(tableStyles.headerCell as any)({})]);
|
|
118
|
+
const thRef = useRef<HTMLTableCellElement>(null);
|
|
119
|
+
|
|
120
|
+
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
const th = thRef.current;
|
|
124
|
+
if (!th) return;
|
|
125
|
+
|
|
126
|
+
const startX = e.clientX;
|
|
127
|
+
const startWidth = th.getBoundingClientRect().width;
|
|
128
|
+
|
|
129
|
+
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
130
|
+
const newWidth = Math.max(minWidth, startWidth + (moveEvent.clientX - startX));
|
|
131
|
+
th.style.width = `${newWidth}px`;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handlePointerUp = (_upEvent: PointerEvent) => {
|
|
135
|
+
document.removeEventListener('pointermove', handlePointerMove);
|
|
136
|
+
document.removeEventListener('pointerup', handlePointerUp);
|
|
137
|
+
const finalWidth = th.getBoundingClientRect().width;
|
|
138
|
+
onResize?.(finalWidth);
|
|
139
|
+
// Remove inline cursor override
|
|
140
|
+
document.body.style.cursor = '';
|
|
141
|
+
document.body.style.userSelect = '';
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Prevent text selection and set resize cursor globally during drag
|
|
145
|
+
document.body.style.cursor = 'col-resize';
|
|
146
|
+
document.body.style.userSelect = 'none';
|
|
147
|
+
document.addEventListener('pointermove', handlePointerMove);
|
|
148
|
+
document.addEventListener('pointerup', handlePointerUp);
|
|
149
|
+
}, [minWidth, onResize]);
|
|
87
150
|
|
|
88
151
|
return (
|
|
89
152
|
<th
|
|
90
153
|
{...headerCellProps}
|
|
154
|
+
ref={thRef}
|
|
91
155
|
scope="col"
|
|
92
156
|
aria-sort={accessibilitySort}
|
|
93
|
-
style={{ width }}
|
|
157
|
+
style={{ width, ...getStickyStyle(sticky, stickyOffset, 11) }}
|
|
94
158
|
>
|
|
95
159
|
{children}
|
|
160
|
+
{resizable && (
|
|
161
|
+
<span
|
|
162
|
+
onPointerDown={handlePointerDown}
|
|
163
|
+
style={{
|
|
164
|
+
position: 'absolute',
|
|
165
|
+
right: 0,
|
|
166
|
+
top: 0,
|
|
167
|
+
bottom: 0,
|
|
168
|
+
width: 4,
|
|
169
|
+
cursor: 'col-resize',
|
|
170
|
+
userSelect: 'none',
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
96
174
|
</th>
|
|
97
175
|
);
|
|
98
176
|
}
|
|
@@ -107,6 +185,8 @@ function TD({
|
|
|
107
185
|
type = 'standard',
|
|
108
186
|
align = 'left',
|
|
109
187
|
width,
|
|
188
|
+
sticky,
|
|
189
|
+
stickyOffset,
|
|
110
190
|
}: TDProps) {
|
|
111
191
|
tableStyles.useVariants({
|
|
112
192
|
size,
|
|
@@ -119,7 +199,7 @@ function TD({
|
|
|
119
199
|
return (
|
|
120
200
|
<td
|
|
121
201
|
{...cellProps}
|
|
122
|
-
style={{ width }}
|
|
202
|
+
style={{ width, ...getStickyStyle(sticky, stickyOffset, 1) }}
|
|
123
203
|
>
|
|
124
204
|
{children}
|
|
125
205
|
</td>
|
|
@@ -136,6 +216,8 @@ interface TFProps {
|
|
|
136
216
|
type?: TableType;
|
|
137
217
|
align?: TableAlignVariant;
|
|
138
218
|
width?: number | string;
|
|
219
|
+
sticky?: boolean | 'left' | 'right';
|
|
220
|
+
stickyOffset?: number | string;
|
|
139
221
|
}
|
|
140
222
|
|
|
141
223
|
function TF({
|
|
@@ -144,6 +226,8 @@ function TF({
|
|
|
144
226
|
type = 'standard',
|
|
145
227
|
align = 'left',
|
|
146
228
|
width,
|
|
229
|
+
sticky,
|
|
230
|
+
stickyOffset,
|
|
147
231
|
}: TFProps) {
|
|
148
232
|
tableStyles.useVariants({
|
|
149
233
|
size,
|
|
@@ -156,7 +240,7 @@ function TF({
|
|
|
156
240
|
return (
|
|
157
241
|
<td
|
|
158
242
|
{...footerCellProps}
|
|
159
|
-
style={{ width }}
|
|
243
|
+
style={{ width, ...getStickyStyle(sticky, stickyOffset, 1) }}
|
|
160
244
|
>
|
|
161
245
|
{children}
|
|
162
246
|
</td>
|
|
@@ -176,8 +260,10 @@ function Table<T = any>({
|
|
|
176
260
|
data,
|
|
177
261
|
type = 'standard',
|
|
178
262
|
size = 'md',
|
|
179
|
-
stickyHeader
|
|
263
|
+
stickyHeader = false,
|
|
180
264
|
onRowPress,
|
|
265
|
+
onColumnResize,
|
|
266
|
+
emptyState,
|
|
181
267
|
// Spacing variants from ContainerStyleProps
|
|
182
268
|
gap,
|
|
183
269
|
padding,
|
|
@@ -241,10 +327,39 @@ function Table<T = any>({
|
|
|
241
327
|
return column.footer;
|
|
242
328
|
};
|
|
243
329
|
|
|
330
|
+
// Compute cumulative offsets for sticky columns (left and right independently)
|
|
331
|
+
const stickyOffsetMap = useMemo(() => {
|
|
332
|
+
const map = new Map<string, number>();
|
|
333
|
+
|
|
334
|
+
// Left sticky: accumulate left-to-right
|
|
335
|
+
let cumulativeLeft = 0;
|
|
336
|
+
for (const col of columns) {
|
|
337
|
+
const side = col.sticky === 'right' ? 'right' : col.sticky ? 'left' : null;
|
|
338
|
+
if (side !== 'left') continue;
|
|
339
|
+
map.set(col.key, cumulativeLeft);
|
|
340
|
+
if (typeof col.width === 'number') {
|
|
341
|
+
cumulativeLeft += col.width;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Right sticky: accumulate right-to-left
|
|
346
|
+
let cumulativeRight = 0;
|
|
347
|
+
for (let i = columns.length - 1; i >= 0; i--) {
|
|
348
|
+
const col = columns[i];
|
|
349
|
+
if (col.sticky !== 'right') continue;
|
|
350
|
+
map.set(col.key, cumulativeRight);
|
|
351
|
+
if (typeof col.width === 'number') {
|
|
352
|
+
cumulativeRight += col.width;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return map;
|
|
357
|
+
}, [columns]);
|
|
358
|
+
|
|
244
359
|
return (
|
|
245
360
|
<div {...containerProps} {...ariaProps} id={id} data-testid={testID}>
|
|
246
361
|
<table {...tableProps} role="table">
|
|
247
|
-
<thead {...getWebProps([(tableStyles.thead as any)({})])}>
|
|
362
|
+
<thead {...getWebProps([(tableStyles.thead as any)({ sticky: stickyHeader })])}>
|
|
248
363
|
<tr>
|
|
249
364
|
{columns.map((column) => (
|
|
250
365
|
<TH
|
|
@@ -253,6 +368,11 @@ function Table<T = any>({
|
|
|
253
368
|
type={type}
|
|
254
369
|
align={column.align}
|
|
255
370
|
width={column.width}
|
|
371
|
+
sticky={column.sticky}
|
|
372
|
+
stickyOffset={stickyOffsetMap.get(column.key)}
|
|
373
|
+
resizable={column.resizable}
|
|
374
|
+
minWidth={column.minWidth}
|
|
375
|
+
onResize={onColumnResize ? (w) => onColumnResize(column.key, w) : undefined}
|
|
256
376
|
accessibilitySort={column.accessibilitySort}
|
|
257
377
|
>
|
|
258
378
|
{column.title}
|
|
@@ -261,28 +381,38 @@ function Table<T = any>({
|
|
|
261
381
|
</tr>
|
|
262
382
|
</thead>
|
|
263
383
|
<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
|
-
|
|
384
|
+
{data.length === 0 && emptyState ? (
|
|
385
|
+
<tr>
|
|
386
|
+
<td colSpan={columns.length} style={{ textAlign: 'center' }}>
|
|
387
|
+
{emptyState}
|
|
388
|
+
</td>
|
|
389
|
+
</tr>
|
|
390
|
+
) : (
|
|
391
|
+
data.map((row, rowIndex) => (
|
|
392
|
+
<TR
|
|
393
|
+
key={rowIndex}
|
|
394
|
+
size={size}
|
|
395
|
+
type={type}
|
|
396
|
+
clickable={isClickable}
|
|
397
|
+
onClick={() => onRowPress?.(row, rowIndex)}
|
|
398
|
+
testID={testID ? `${testID}-row-${rowIndex}` : undefined}
|
|
399
|
+
>
|
|
400
|
+
{columns.map((column) => (
|
|
401
|
+
<TD
|
|
402
|
+
key={column.key}
|
|
403
|
+
size={size}
|
|
404
|
+
type={type}
|
|
405
|
+
align={column.align}
|
|
406
|
+
width={column.width}
|
|
407
|
+
sticky={column.sticky}
|
|
408
|
+
stickyOffset={stickyOffsetMap.get(column.key)}
|
|
409
|
+
>
|
|
410
|
+
{getCellValue(column, row, rowIndex)}
|
|
411
|
+
</TD>
|
|
412
|
+
))}
|
|
413
|
+
</TR>
|
|
414
|
+
))
|
|
415
|
+
)}
|
|
286
416
|
</tbody>
|
|
287
417
|
{hasFooter && (
|
|
288
418
|
<tfoot {...getWebProps([(tableStyles.tfoot as any)({})])}>
|
|
@@ -294,6 +424,8 @@ function Table<T = any>({
|
|
|
294
424
|
type={type}
|
|
295
425
|
align={column.align}
|
|
296
426
|
width={column.width}
|
|
427
|
+
sticky={column.sticky}
|
|
428
|
+
stickyOffset={stickyOffsetMap.get(column.key)}
|
|
297
429
|
>
|
|
298
430
|
{getFooterContent(column) ?? null}
|
|
299
431
|
</TF>
|
package/src/Table/types.ts
CHANGED
|
@@ -17,6 +17,21 @@ 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
|
+
* `true` or `'left'` pins to the left, `'right'` pins to the right.
|
|
23
|
+
* On web uses CSS `position: sticky`, on native renders outside the ScrollView.
|
|
24
|
+
*/
|
|
25
|
+
sticky?: boolean | 'left' | 'right';
|
|
26
|
+
/**
|
|
27
|
+
* Allows the column to be resized by dragging the right edge of the header.
|
|
28
|
+
* Web only.
|
|
29
|
+
*/
|
|
30
|
+
resizable?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Minimum width when resizing (default: 50).
|
|
33
|
+
*/
|
|
34
|
+
minWidth?: number;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
/**
|
|
@@ -33,6 +48,16 @@ export interface TableProps<T = any> extends ContainerStyleProps, AccessibilityP
|
|
|
33
48
|
size?: TableSizeVariant;
|
|
34
49
|
stickyHeader?: boolean;
|
|
35
50
|
onRowPress?: (row: T, index: number) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Called when a column is resized via drag handle.
|
|
53
|
+
* Receives the column key and the new width in pixels.
|
|
54
|
+
*/
|
|
55
|
+
onColumnResize?: (key: string, width: number) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Content to display when `data` is empty.
|
|
58
|
+
* Renders in place of the table body.
|
|
59
|
+
*/
|
|
60
|
+
emptyState?: ReactNode;
|
|
36
61
|
style?: StyleProp<ViewStyle>;
|
|
37
62
|
testID?: string;
|
|
38
63
|
}
|
|
@@ -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;
|