@cdx-ui/components 0.0.1-beta.184 → 0.0.1-beta.185

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/coverage.svg +1 -1
  2. package/lib/commonjs/components/Table/index.js +404 -0
  3. package/lib/commonjs/components/Table/index.js.map +1 -0
  4. package/lib/commonjs/components/Table/index.web.js +295 -0
  5. package/lib/commonjs/components/Table/index.web.js.map +1 -0
  6. package/lib/commonjs/components/Table/styles.js +63 -0
  7. package/lib/commonjs/components/Table/styles.js.map +1 -0
  8. package/lib/commonjs/components/Table/types.js +6 -0
  9. package/lib/commonjs/components/Table/types.js.map +1 -0
  10. package/lib/commonjs/components/Table/useTableStyleContext.js +28 -0
  11. package/lib/commonjs/components/Table/useTableStyleContext.js.map +1 -0
  12. package/lib/commonjs/components/index.js +12 -0
  13. package/lib/commonjs/components/index.js.map +1 -1
  14. package/lib/module/components/Table/index.js +400 -0
  15. package/lib/module/components/Table/index.js.map +1 -0
  16. package/lib/module/components/Table/index.web.js +291 -0
  17. package/lib/module/components/Table/index.web.js.map +1 -0
  18. package/lib/module/components/Table/styles.js +59 -0
  19. package/lib/module/components/Table/styles.js.map +1 -0
  20. package/lib/module/components/Table/types.js +4 -0
  21. package/lib/module/components/Table/types.js.map +1 -0
  22. package/lib/module/components/Table/useTableStyleContext.js +17 -0
  23. package/lib/module/components/Table/useTableStyleContext.js.map +1 -0
  24. package/lib/module/components/index.js +1 -0
  25. package/lib/module/components/index.js.map +1 -1
  26. package/lib/typescript/components/Table/index.d.ts +23 -0
  27. package/lib/typescript/components/Table/index.d.ts.map +1 -0
  28. package/lib/typescript/components/Table/index.web.d.ts +22 -0
  29. package/lib/typescript/components/Table/index.web.d.ts.map +1 -0
  30. package/lib/typescript/components/Table/styles.d.ts +26 -0
  31. package/lib/typescript/components/Table/styles.d.ts.map +1 -0
  32. package/lib/typescript/components/Table/types.d.ts +39 -0
  33. package/lib/typescript/components/Table/types.d.ts.map +1 -0
  34. package/lib/typescript/components/Table/useTableStyleContext.d.ts +11 -0
  35. package/lib/typescript/components/Table/useTableStyleContext.d.ts.map +1 -0
  36. package/lib/typescript/components/index.d.ts +1 -0
  37. package/lib/typescript/components/index.d.ts.map +1 -1
  38. package/package.json +5 -5
  39. package/src/components/Table/index.tsx +489 -0
  40. package/src/components/Table/index.web.tsx +367 -0
  41. package/src/components/Table/styles.ts +90 -0
  42. package/src/components/Table/types.ts +44 -0
  43. package/src/components/Table/useTableStyleContext.ts +14 -0
  44. package/src/components/index.ts +1 -0
@@ -0,0 +1,489 @@
1
+ import {
2
+ Children,
3
+ createContext,
4
+ forwardRef,
5
+ isValidElement,
6
+ useCallback,
7
+ useContext,
8
+ useId,
9
+ useLayoutEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ type ReactElement,
14
+ type ReactNode,
15
+ } from 'react';
16
+ import { ScrollView, Text, View, type LayoutChangeEvent } from 'react-native';
17
+ import { Slot, dataAttributes, mergeDataAttributes } from '@cdx-ui/primitives';
18
+ import { cn } from '@cdx-ui/utils';
19
+ import {
20
+ tableNativeCaptionVariants,
21
+ tableContainerVariants,
22
+ tableNativeCellVariants,
23
+ tableNativeFooterVariants,
24
+ tableNativeHeadVariants,
25
+ tableNativeRootVariants,
26
+ tableNativeRowVariants,
27
+ } from './styles';
28
+ import type {
29
+ TableCaptionProps,
30
+ TableCellProps,
31
+ TableHeadProps,
32
+ TableProps,
33
+ TableRowProps,
34
+ TableSectionProps,
35
+ } from './types';
36
+ import {
37
+ ParentContext,
38
+ useTableStyleContext,
39
+ useTableVariantContext,
40
+ } from './useTableStyleContext';
41
+
42
+ export type {
43
+ TableCaptionProps,
44
+ TableCellProps,
45
+ TableHeadProps,
46
+ TableProps,
47
+ TableRowProps,
48
+ TableSectionProps,
49
+ } from './types';
50
+ export type { TableVariantProps } from './styles';
51
+
52
+ const NATIVE_COL_MIN_WIDTH = 96;
53
+
54
+ interface TableLayoutValue {
55
+ tableWidth: number;
56
+ columnCount: number;
57
+ registerRowColumnCount: (rowId: string, count: number) => void;
58
+ unregisterRowColumnCount: (rowId: string) => void;
59
+ setViewportWidth: (width: number) => void;
60
+ }
61
+
62
+ const TableLayoutContext = createContext<TableLayoutValue>({
63
+ tableWidth: 0,
64
+ columnCount: 0,
65
+ registerRowColumnCount: () => {},
66
+ unregisterRowColumnCount: () => {},
67
+ setViewportWidth: () => {},
68
+ });
69
+
70
+ const useTableLayout = () => useContext(TableLayoutContext);
71
+
72
+ const nativeColumnStyle = (span: number, tableWidth: number, columnCount: number) => {
73
+ if (tableWidth <= 0 || columnCount <= 0) {
74
+ return undefined;
75
+ }
76
+
77
+ return {
78
+ width: (span / columnCount) * tableWidth,
79
+ flexGrow: 0,
80
+ flexShrink: 0,
81
+ };
82
+ };
83
+
84
+ const isTableColumnChild = (child: ReactNode): child is ReactElement<{ colSpan?: number }> =>
85
+ isValidElement(child) &&
86
+ ['Table.Head', 'Table.Cell'].includes((child.type as { displayName?: string }).displayName ?? '');
87
+
88
+ const countRowColumns = (children: ReactNode): number =>
89
+ Children.toArray(children).reduce<number>((total, child) => {
90
+ if (!isTableColumnChild(child)) {
91
+ return total;
92
+ }
93
+ const span = child.props.colSpan;
94
+ return total + (span && span > 1 ? span : 1);
95
+ }, 0);
96
+
97
+ const columnSpan = (colSpan?: number) => (colSpan && colSpan > 1 ? colSpan : 1);
98
+
99
+ const nativeCrossAxisAlign = (className?: string) => {
100
+ if (!className) {
101
+ return undefined;
102
+ }
103
+
104
+ if (/\btext-right\b/.test(className)) {
105
+ return 'items-end';
106
+ }
107
+
108
+ if (/\btext-center\b/.test(className)) {
109
+ return 'items-center';
110
+ }
111
+
112
+ return undefined;
113
+ };
114
+
115
+ const isCaptionChild = (child: ReactNode): child is ReactElement =>
116
+ isValidElement(child) && (child.type as { displayName?: string }).displayName === 'Table.Caption';
117
+
118
+ const nativeTextAlign = (className?: string) => {
119
+ if (!className) {
120
+ return undefined;
121
+ }
122
+
123
+ if (/\btext-right\b/.test(className)) {
124
+ return 'text-right';
125
+ }
126
+
127
+ if (/\btext-center\b/.test(className)) {
128
+ return 'text-center';
129
+ }
130
+
131
+ if (/\btext-left\b/.test(className)) {
132
+ return 'text-left';
133
+ }
134
+
135
+ return undefined;
136
+ };
137
+
138
+ const isTextChild = (child: ReactNode): child is string | number =>
139
+ typeof child === 'string' || typeof child === 'number';
140
+
141
+ const renderNativeTableTextContent = (children: ReactNode, textClassName: string): ReactNode => {
142
+ if (isTextChild(children)) {
143
+ return (
144
+ <Text numberOfLines={1} ellipsizeMode="tail" className={textClassName}>
145
+ {children}
146
+ </Text>
147
+ );
148
+ }
149
+
150
+ const childList = Children.toArray(children);
151
+
152
+ if (childList.length === 0) {
153
+ return children;
154
+ }
155
+
156
+ if (childList.every(isTextChild)) {
157
+ return (
158
+ <Text numberOfLines={1} ellipsizeMode="tail" className={textClassName}>
159
+ {childList}
160
+ </Text>
161
+ );
162
+ }
163
+
164
+ return Children.map(children, (child, index) => {
165
+ if (!isTextChild(child)) {
166
+ return child;
167
+ }
168
+
169
+ return (
170
+ <Text
171
+ key={`table-text-${index.toString()}`}
172
+ numberOfLines={1}
173
+ ellipsizeMode="tail"
174
+ className={textClassName}
175
+ >
176
+ {child}
177
+ </Text>
178
+ );
179
+ });
180
+ };
181
+
182
+ const TableRoot = forwardRef<View, TableProps>(
183
+ ({ asChild, className, children, style, fullBleed = false, ...props }, ref) => {
184
+ const ctx = useTableVariantContext(fullBleed);
185
+ const computedClassName = cn(tableContainerVariants(), className);
186
+ const [viewportWidth, setViewportWidth] = useState(0);
187
+ const [columnCount, setColumnCount] = useState(0);
188
+ const rowColumnCountsRef = useRef(new Map<string, number>());
189
+ const syncColumnCount = useCallback(() => {
190
+ let max = 0;
191
+ for (const count of rowColumnCountsRef.current.values()) {
192
+ if (count > max) {
193
+ max = count;
194
+ }
195
+ }
196
+ setColumnCount((prev) => (prev === max ? prev : max));
197
+ }, []);
198
+ const registerRowColumnCount = useCallback(
199
+ (rowId: string, count: number) => {
200
+ rowColumnCountsRef.current.set(rowId, count);
201
+ syncColumnCount();
202
+ },
203
+ [syncColumnCount],
204
+ );
205
+ const unregisterRowColumnCount = useCallback(
206
+ (rowId: string) => {
207
+ if (rowColumnCountsRef.current.delete(rowId)) {
208
+ syncColumnCount();
209
+ }
210
+ },
211
+ [syncColumnCount],
212
+ );
213
+ const tableWidth = Math.max(viewportWidth, columnCount * NATIVE_COL_MIN_WIDTH);
214
+ const layout = useMemo(
215
+ () => ({
216
+ tableWidth,
217
+ columnCount,
218
+ registerRowColumnCount,
219
+ unregisterRowColumnCount,
220
+ setViewportWidth,
221
+ }),
222
+ [tableWidth, columnCount, registerRowColumnCount, unregisterRowColumnCount],
223
+ );
224
+
225
+ const handleViewportLayout = (event: LayoutChangeEvent) => {
226
+ const nextWidth = event.nativeEvent.layout.width;
227
+ setViewportWidth((prev) => (prev === nextWidth ? prev : nextWidth));
228
+ };
229
+
230
+ if (asChild) {
231
+ const Comp = Slot;
232
+ return (
233
+ <ParentContext.Provider value={ctx}>
234
+ <TableLayoutContext.Provider value={layout}>
235
+ <Comp
236
+ ref={ref}
237
+ className={computedClassName}
238
+ style={style}
239
+ {...props}
240
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table' }))}
241
+ >
242
+ {children}
243
+ </Comp>
244
+ </TableLayoutContext.Provider>
245
+ </ParentContext.Provider>
246
+ );
247
+ }
248
+
249
+ const childList = Children.toArray(children);
250
+ const captions = childList.filter(isCaptionChild);
251
+ const tableChildren = childList.filter((child) => !isCaptionChild(child));
252
+
253
+ return (
254
+ <ParentContext.Provider value={ctx}>
255
+ <TableLayoutContext.Provider value={layout}>
256
+ <ScrollView
257
+ horizontal
258
+ showsHorizontalScrollIndicator
259
+ className={computedClassName}
260
+ style={style}
261
+ onLayout={handleViewportLayout}
262
+ >
263
+ <View
264
+ ref={ref}
265
+ className={tableNativeRootVariants()}
266
+ style={tableWidth > 0 ? { width: tableWidth } : undefined}
267
+ accessibilityRole="none"
268
+ {...props}
269
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table' }))}
270
+ >
271
+ {tableChildren}
272
+ {captions}
273
+ </View>
274
+ </ScrollView>
275
+ </TableLayoutContext.Provider>
276
+ </ParentContext.Provider>
277
+ );
278
+ },
279
+ );
280
+
281
+ TableRoot.displayName = 'Table';
282
+
283
+ const TableCaption = forwardRef<Text, TableCaptionProps>(
284
+ ({ asChild, className, children, style, ...props }, ref) => {
285
+ const computedClassName = cn(tableNativeCaptionVariants(), className);
286
+ const Comp = asChild ? Slot : Text;
287
+ const { tableWidth } = useTableLayout();
288
+
289
+ return (
290
+ <Comp
291
+ ref={ref}
292
+ className={computedClassName}
293
+ style={[tableWidth > 0 ? { width: tableWidth } : null, style]}
294
+ accessibilityRole="text"
295
+ {...props}
296
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table-caption' }))}
297
+ >
298
+ {children}
299
+ </Comp>
300
+ );
301
+ },
302
+ );
303
+
304
+ TableCaption.displayName = 'Table.Caption';
305
+
306
+ const TableHeader = forwardRef<View, TableSectionProps>(
307
+ ({ asChild, className, children, style, ...props }, ref) => {
308
+ const Comp = asChild ? Slot : View;
309
+
310
+ return (
311
+ <Comp
312
+ ref={ref}
313
+ className={className}
314
+ style={style}
315
+ accessibilityRole="header"
316
+ {...props}
317
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table-header' }))}
318
+ >
319
+ {children}
320
+ </Comp>
321
+ );
322
+ },
323
+ );
324
+
325
+ TableHeader.displayName = 'Table.Header';
326
+
327
+ const TableBody = forwardRef<View, TableSectionProps>(
328
+ ({ asChild, className, children, style, ...props }, ref) => {
329
+ const Comp = asChild ? Slot : View;
330
+
331
+ return (
332
+ <Comp
333
+ ref={ref}
334
+ className={className}
335
+ style={style}
336
+ {...props}
337
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table-body' }))}
338
+ >
339
+ {children}
340
+ </Comp>
341
+ );
342
+ },
343
+ );
344
+
345
+ TableBody.displayName = 'Table.Body';
346
+
347
+ const TableFooter = forwardRef<View, TableSectionProps>(
348
+ ({ asChild, className, children, style, ...props }, ref) => {
349
+ const computedClassName = cn(tableNativeFooterVariants(), className);
350
+ const Comp = asChild ? Slot : View;
351
+
352
+ return (
353
+ <Comp
354
+ ref={ref}
355
+ className={computedClassName}
356
+ style={style}
357
+ accessibilityRole="summary"
358
+ {...props}
359
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table-footer' }))}
360
+ >
361
+ {children}
362
+ </Comp>
363
+ );
364
+ },
365
+ );
366
+
367
+ TableFooter.displayName = 'Table.Footer';
368
+
369
+ const TableRow = forwardRef<View, TableRowProps>(
370
+ ({ asChild, className, children, style, ...props }, ref) => {
371
+ const computedClassName = cn(tableNativeRowVariants(), className);
372
+ const Comp = asChild ? Slot : View;
373
+ const rowId = useId();
374
+ const { tableWidth, registerRowColumnCount, unregisterRowColumnCount } = useTableLayout();
375
+ const columnCount = countRowColumns(children);
376
+
377
+ useLayoutEffect(() => {
378
+ if (columnCount > 0) {
379
+ registerRowColumnCount(rowId, columnCount);
380
+ } else {
381
+ unregisterRowColumnCount(rowId);
382
+ }
383
+
384
+ return () => {
385
+ unregisterRowColumnCount(rowId);
386
+ };
387
+ }, [rowId, columnCount, registerRowColumnCount, unregisterRowColumnCount]);
388
+
389
+ return (
390
+ <Comp
391
+ ref={ref}
392
+ className={computedClassName}
393
+ style={[tableWidth > 0 ? { width: tableWidth } : null, style]}
394
+ accessibilityRole="none"
395
+ {...props}
396
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table-row' }))}
397
+ >
398
+ {children}
399
+ </Comp>
400
+ );
401
+ },
402
+ );
403
+
404
+ TableRow.displayName = 'Table.Row';
405
+
406
+ const TableHead = forwardRef<View, TableHeadProps>(
407
+ (
408
+ { asChild, className, children, style, colSpan, rowSpan: _rowSpan, scope: _scope, ...props },
409
+ ref,
410
+ ) => {
411
+ const { fullBleed } = useTableStyleContext();
412
+ const { tableWidth, columnCount } = useTableLayout();
413
+ const computedClassName = cn(
414
+ tableNativeHeadVariants({ fullBleed }),
415
+ nativeCrossAxisAlign(className),
416
+ className,
417
+ );
418
+ const Comp = asChild ? Slot : View;
419
+
420
+ return (
421
+ <Comp
422
+ ref={ref}
423
+ className={computedClassName}
424
+ style={[nativeColumnStyle(columnSpan(colSpan), tableWidth, columnCount), style]}
425
+ accessibilityRole="header"
426
+ {...props}
427
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table-head' }))}
428
+ >
429
+ {renderNativeTableTextContent(
430
+ children,
431
+ cn('w-full font-medium text-content-secondary body-md', nativeTextAlign(className)),
432
+ )}
433
+ </Comp>
434
+ );
435
+ },
436
+ );
437
+
438
+ TableHead.displayName = 'Table.Head';
439
+
440
+ const TableCell = forwardRef<View, TableCellProps>(
441
+ ({ asChild, className, children, style, colSpan, rowSpan: _rowSpan, ...props }, ref) => {
442
+ const { fullBleed } = useTableStyleContext();
443
+ const { tableWidth, columnCount } = useTableLayout();
444
+ const computedClassName = cn(
445
+ tableNativeCellVariants({ fullBleed }),
446
+ nativeCrossAxisAlign(className),
447
+ className,
448
+ );
449
+ const Comp = asChild ? Slot : View;
450
+
451
+ return (
452
+ <Comp
453
+ ref={ref}
454
+ className={computedClassName}
455
+ style={[nativeColumnStyle(columnSpan(colSpan), tableWidth, columnCount), style]}
456
+ accessibilityRole="text"
457
+ {...props}
458
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'table-cell' }))}
459
+ >
460
+ {renderNativeTableTextContent(
461
+ children,
462
+ cn('w-full body-md text-content-primary', nativeTextAlign(className)),
463
+ )}
464
+ </Comp>
465
+ );
466
+ },
467
+ );
468
+
469
+ TableCell.displayName = 'Table.Cell';
470
+
471
+ type TableCompoundComponent = typeof TableRoot & {
472
+ Caption: typeof TableCaption;
473
+ Header: typeof TableHeader;
474
+ Body: typeof TableBody;
475
+ Footer: typeof TableFooter;
476
+ Row: typeof TableRow;
477
+ Head: typeof TableHead;
478
+ Cell: typeof TableCell;
479
+ };
480
+
481
+ export const Table = Object.assign(TableRoot, {
482
+ Caption: TableCaption,
483
+ Header: TableHeader,
484
+ Body: TableBody,
485
+ Footer: TableFooter,
486
+ Row: TableRow,
487
+ Head: TableHead,
488
+ Cell: TableCell,
489
+ }) as TableCompoundComponent;