@idealyst/components 1.2.123 → 1.2.125
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/ScrollView/ScrollView.native.tsx +172 -0
- package/src/ScrollView/ScrollView.styles.tsx +104 -0
- package/src/ScrollView/ScrollView.web.tsx +242 -0
- package/src/ScrollView/docs.ts +9 -0
- package/src/ScrollView/index.native.ts +2 -0
- package/src/ScrollView/index.ts +5 -0
- package/src/ScrollView/index.web.ts +5 -0
- package/src/ScrollView/types.ts +169 -0
- package/src/TabBar/TabBar.styles.tsx +13 -13
- package/src/Table/Table.native.tsx +76 -3
- package/src/Table/Table.styles.tsx +40 -0
- package/src/Table/Table.web.tsx +63 -0
- package/src/Table/types.ts +2 -1
- package/src/View/types.ts +4 -0
- package/src/index.native.ts +4 -0
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.125",
|
|
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.125",
|
|
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.125",
|
|
115
|
+
"@idealyst/tooling": "^1.2.125",
|
|
116
116
|
"@mdi/react": "^1.6.1",
|
|
117
117
|
"@types/react": "^19.1.0",
|
|
118
118
|
"react": "^19.1.0",
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ScrollView as RNScrollView,
|
|
4
|
+
NativeScrollEvent,
|
|
5
|
+
NativeSyntheticEvent,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import { scrollViewStyles } from './ScrollView.styles';
|
|
9
|
+
import type { ScrollViewProps, ScrollViewRef, ScrollEvent } from './types';
|
|
10
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
11
|
+
|
|
12
|
+
function buildScrollEvent(e: NativeScrollEvent): ScrollEvent {
|
|
13
|
+
const { contentOffset, contentSize, layoutMeasurement } = e;
|
|
14
|
+
const isVerticalEnd =
|
|
15
|
+
contentOffset.y + layoutMeasurement.height >= contentSize.height - 1;
|
|
16
|
+
const isHorizontalEnd =
|
|
17
|
+
contentOffset.x + layoutMeasurement.width >= contentSize.width - 1;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
position: { x: contentOffset.x, y: contentOffset.y },
|
|
21
|
+
contentSize: { width: contentSize.width, height: contentSize.height },
|
|
22
|
+
layoutSize: { width: layoutMeasurement.width, height: layoutMeasurement.height },
|
|
23
|
+
isAtEnd: isVerticalEnd || isHorizontalEnd,
|
|
24
|
+
isAtStart: contentOffset.x <= 0 && contentOffset.y <= 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ScrollView = forwardRef<ScrollViewRef, ScrollViewProps>(({
|
|
29
|
+
children,
|
|
30
|
+
direction = 'vertical',
|
|
31
|
+
background = 'transparent',
|
|
32
|
+
radius = 'none',
|
|
33
|
+
border = 'none',
|
|
34
|
+
gap,
|
|
35
|
+
padding,
|
|
36
|
+
paddingVertical,
|
|
37
|
+
paddingHorizontal,
|
|
38
|
+
margin,
|
|
39
|
+
marginVertical,
|
|
40
|
+
marginHorizontal,
|
|
41
|
+
backgroundColor,
|
|
42
|
+
borderRadius,
|
|
43
|
+
borderWidth,
|
|
44
|
+
borderColor,
|
|
45
|
+
style,
|
|
46
|
+
contentContainerStyle,
|
|
47
|
+
showsIndicator = true,
|
|
48
|
+
pagingEnabled = false,
|
|
49
|
+
bounces = true,
|
|
50
|
+
onScroll,
|
|
51
|
+
onScrollBegin,
|
|
52
|
+
onScrollEnd,
|
|
53
|
+
onEndReached,
|
|
54
|
+
onEndReachedThreshold = 0,
|
|
55
|
+
scrollEventThrottle = 16,
|
|
56
|
+
scrollEnabled = true,
|
|
57
|
+
keyboardDismissMode = 'none',
|
|
58
|
+
testID,
|
|
59
|
+
id,
|
|
60
|
+
onLayout,
|
|
61
|
+
}, ref) => {
|
|
62
|
+
const scrollRef = useRef<RNScrollView>(null);
|
|
63
|
+
const lastPositionRef = useRef({ x: 0, y: 0 });
|
|
64
|
+
const endReachedRef = useRef(false);
|
|
65
|
+
|
|
66
|
+
const backgroundVariant = background === 'transparent' ? undefined : background;
|
|
67
|
+
|
|
68
|
+
scrollViewStyles.useVariants({
|
|
69
|
+
background: backgroundVariant,
|
|
70
|
+
radius,
|
|
71
|
+
border,
|
|
72
|
+
gap,
|
|
73
|
+
padding,
|
|
74
|
+
paddingVertical,
|
|
75
|
+
paddingHorizontal,
|
|
76
|
+
margin,
|
|
77
|
+
marginVertical,
|
|
78
|
+
marginHorizontal,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const containerStyle = (scrollViewStyles.container as any)({});
|
|
82
|
+
const contentStyle = (scrollViewStyles.contentContainer as any)({});
|
|
83
|
+
|
|
84
|
+
const overrideStyles: ViewStyle = {};
|
|
85
|
+
if (backgroundColor) overrideStyles.backgroundColor = backgroundColor;
|
|
86
|
+
if (borderRadius !== undefined) overrideStyles.borderRadius = borderRadius;
|
|
87
|
+
if (borderWidth !== undefined) overrideStyles.borderWidth = borderWidth;
|
|
88
|
+
if (borderColor) overrideStyles.borderColor = borderColor;
|
|
89
|
+
|
|
90
|
+
// Imperative handle
|
|
91
|
+
useImperativeHandle(ref, () => ({
|
|
92
|
+
scrollTo: (options) => {
|
|
93
|
+
scrollRef.current?.scrollTo({
|
|
94
|
+
x: options.x ?? 0,
|
|
95
|
+
y: options.y ?? 0,
|
|
96
|
+
animated: options.animated ?? true,
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
scrollToEnd: (options) => {
|
|
100
|
+
scrollRef.current?.scrollToEnd({ animated: options?.animated ?? true });
|
|
101
|
+
},
|
|
102
|
+
scrollToStart: (options) => {
|
|
103
|
+
scrollRef.current?.scrollTo({
|
|
104
|
+
x: 0,
|
|
105
|
+
y: 0,
|
|
106
|
+
animated: options?.animated ?? true,
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
getScrollPosition: () => lastPositionRef.current,
|
|
110
|
+
getInnerElement: () => scrollRef.current,
|
|
111
|
+
}), []);
|
|
112
|
+
|
|
113
|
+
const handleScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
114
|
+
const event = buildScrollEvent(e.nativeEvent);
|
|
115
|
+
lastPositionRef.current = event.position;
|
|
116
|
+
|
|
117
|
+
onScroll?.(event);
|
|
118
|
+
|
|
119
|
+
// End reached detection
|
|
120
|
+
if (onEndReached) {
|
|
121
|
+
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
122
|
+
const distanceFromEnd = direction === 'horizontal'
|
|
123
|
+
? contentSize.width - layoutMeasurement.width - contentOffset.x
|
|
124
|
+
: contentSize.height - layoutMeasurement.height - contentOffset.y;
|
|
125
|
+
|
|
126
|
+
if (distanceFromEnd <= onEndReachedThreshold && !endReachedRef.current) {
|
|
127
|
+
endReachedRef.current = true;
|
|
128
|
+
onEndReached();
|
|
129
|
+
} else if (distanceFromEnd > onEndReachedThreshold) {
|
|
130
|
+
endReachedRef.current = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, [onScroll, onEndReached, onEndReachedThreshold, direction]);
|
|
134
|
+
|
|
135
|
+
const handleScrollBeginDrag = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
136
|
+
onScrollBegin?.(buildScrollEvent(e.nativeEvent));
|
|
137
|
+
}, [onScrollBegin]);
|
|
138
|
+
|
|
139
|
+
const handleMomentumScrollEnd = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
140
|
+
onScrollEnd?.(buildScrollEvent(e.nativeEvent));
|
|
141
|
+
}, [onScrollEnd]);
|
|
142
|
+
|
|
143
|
+
const isHorizontal = direction === 'horizontal';
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<RNScrollView
|
|
147
|
+
ref={scrollRef}
|
|
148
|
+
style={[containerStyle, overrideStyles, style]}
|
|
149
|
+
contentContainerStyle={[contentStyle, contentContainerStyle]}
|
|
150
|
+
horizontal={isHorizontal}
|
|
151
|
+
showsVerticalScrollIndicator={!isHorizontal && showsIndicator}
|
|
152
|
+
showsHorizontalScrollIndicator={isHorizontal && showsIndicator}
|
|
153
|
+
pagingEnabled={pagingEnabled}
|
|
154
|
+
bounces={bounces}
|
|
155
|
+
scrollEnabled={scrollEnabled}
|
|
156
|
+
scrollEventThrottle={scrollEventThrottle}
|
|
157
|
+
keyboardDismissMode={keyboardDismissMode}
|
|
158
|
+
onScroll={handleScroll}
|
|
159
|
+
onScrollBeginDrag={onScrollBegin ? handleScrollBeginDrag : undefined}
|
|
160
|
+
onMomentumScrollEnd={onScrollEnd ? handleMomentumScrollEnd : undefined}
|
|
161
|
+
onLayout={onLayout}
|
|
162
|
+
testID={testID}
|
|
163
|
+
nativeID={id}
|
|
164
|
+
>
|
|
165
|
+
{children}
|
|
166
|
+
</RNScrollView>
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
ScrollView.displayName = 'ScrollView';
|
|
171
|
+
|
|
172
|
+
export default ScrollView;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollView styles using defineStyle with $iterator expansion.
|
|
3
|
+
* Reuses the same visual variant patterns as View.
|
|
4
|
+
*/
|
|
5
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
6
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
7
|
+
import type { Theme as BaseTheme } from '@idealyst/theme';
|
|
8
|
+
import { ScrollViewBackgroundVariant, ScrollViewBorderVariant, ScrollViewRadiusVariant } from './types';
|
|
9
|
+
import { ViewStyleSize } from '../utils/viewStyleProps';
|
|
10
|
+
|
|
11
|
+
// Required: Unistyles must see StyleSheet usage in original source to process this file
|
|
12
|
+
void StyleSheet;
|
|
13
|
+
|
|
14
|
+
// Wrap theme for $iterator support
|
|
15
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
16
|
+
|
|
17
|
+
export type ScrollViewVariants = {
|
|
18
|
+
background: ScrollViewBackgroundVariant;
|
|
19
|
+
radius: ScrollViewRadiusVariant;
|
|
20
|
+
border: ScrollViewBorderVariant;
|
|
21
|
+
gap: ViewStyleSize;
|
|
22
|
+
padding: ViewStyleSize;
|
|
23
|
+
paddingVertical: ViewStyleSize;
|
|
24
|
+
paddingHorizontal: ViewStyleSize;
|
|
25
|
+
margin: ViewStyleSize;
|
|
26
|
+
marginVertical: ViewStyleSize;
|
|
27
|
+
marginHorizontal: ViewStyleSize;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ScrollViewDynamicProps = Partial<ScrollViewVariants>;
|
|
31
|
+
|
|
32
|
+
export const scrollViewStyles = defineStyle('ScrollView', (theme: Theme) => ({
|
|
33
|
+
container: (_props: ScrollViewDynamicProps) => ({
|
|
34
|
+
display: 'flex' as const,
|
|
35
|
+
flex: 1,
|
|
36
|
+
borderColor: theme.colors.border.primary,
|
|
37
|
+
borderWidth: 0,
|
|
38
|
+
variants: {
|
|
39
|
+
background: {
|
|
40
|
+
backgroundColor: theme.colors.$surface,
|
|
41
|
+
},
|
|
42
|
+
radius: {
|
|
43
|
+
none: { borderRadius: 0 },
|
|
44
|
+
xs: { borderRadius: 2 },
|
|
45
|
+
sm: { borderRadius: 4 },
|
|
46
|
+
md: { borderRadius: 8 },
|
|
47
|
+
lg: { borderRadius: 12 },
|
|
48
|
+
xl: { borderRadius: 16 },
|
|
49
|
+
},
|
|
50
|
+
border: {
|
|
51
|
+
none: { borderWidth: 0 },
|
|
52
|
+
thin: { borderWidth: 1, borderStyle: 'solid' as const, borderColor: theme.colors.pallet.gray[300] },
|
|
53
|
+
thick: { borderWidth: 2, borderStyle: 'solid' as const, borderColor: theme.colors.pallet.gray[300] },
|
|
54
|
+
},
|
|
55
|
+
margin: {
|
|
56
|
+
margin: theme.sizes.$view.padding,
|
|
57
|
+
},
|
|
58
|
+
marginVertical: {
|
|
59
|
+
marginVertical: theme.sizes.$view.padding,
|
|
60
|
+
},
|
|
61
|
+
marginHorizontal: {
|
|
62
|
+
marginHorizontal: theme.sizes.$view.padding,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
_web: {
|
|
66
|
+
display: 'flex',
|
|
67
|
+
flexDirection: 'column',
|
|
68
|
+
boxSizing: 'border-box',
|
|
69
|
+
borderStyle: 'solid',
|
|
70
|
+
position: 'relative',
|
|
71
|
+
minHeight: 0,
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
contentContainer: (_props: ScrollViewDynamicProps) => ({
|
|
75
|
+
borderColor: theme.colors.border.primary,
|
|
76
|
+
borderWidth: 0,
|
|
77
|
+
variants: {
|
|
78
|
+
gap: {
|
|
79
|
+
gap: theme.sizes.$view.spacing,
|
|
80
|
+
},
|
|
81
|
+
padding: {
|
|
82
|
+
padding: theme.sizes.$view.padding,
|
|
83
|
+
},
|
|
84
|
+
paddingVertical: {
|
|
85
|
+
paddingVertical: theme.sizes.$view.padding,
|
|
86
|
+
},
|
|
87
|
+
paddingHorizontal: {
|
|
88
|
+
paddingHorizontal: theme.sizes.$view.padding,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
_web: {
|
|
92
|
+
boxSizing: 'border-box',
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
// Web-only: the actual scrolling element (absolute positioned within container)
|
|
96
|
+
scrollableRegion: {
|
|
97
|
+
_web: {
|
|
98
|
+
position: 'absolute',
|
|
99
|
+
inset: 0,
|
|
100
|
+
overflow: 'auto',
|
|
101
|
+
boxSizing: 'border-box',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}));
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
|
+
import { scrollViewStyles } from './ScrollView.styles';
|
|
4
|
+
import type { ScrollViewProps, ScrollViewRef, ScrollEvent } from './types';
|
|
5
|
+
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
|
+
import { useWebLayout } from '../hooks/useWebLayout';
|
|
7
|
+
import { flattenStyle } from '../utils/flattenStyle';
|
|
8
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
9
|
+
|
|
10
|
+
function buildScrollEvent(el: HTMLElement): ScrollEvent {
|
|
11
|
+
const position = { x: el.scrollLeft, y: el.scrollTop };
|
|
12
|
+
const contentSize = { width: el.scrollWidth, height: el.scrollHeight };
|
|
13
|
+
const layoutSize = { width: el.clientWidth, height: el.clientHeight };
|
|
14
|
+
const isAtEnd =
|
|
15
|
+
el.scrollTop + el.clientHeight >= el.scrollHeight - 1 ||
|
|
16
|
+
el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
|
|
17
|
+
const isAtStart = el.scrollLeft <= 0 && el.scrollTop <= 0;
|
|
18
|
+
|
|
19
|
+
return { position, contentSize, layoutSize, isAtEnd, isAtStart };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Scrollable container with scroll event abstractions and imperative scroll controls.
|
|
24
|
+
* Web implementation uses a container + absolutely positioned scrollable region.
|
|
25
|
+
*/
|
|
26
|
+
const ScrollView = forwardRef<ScrollViewRef, ScrollViewProps>(({
|
|
27
|
+
children,
|
|
28
|
+
direction = 'vertical',
|
|
29
|
+
background = 'transparent',
|
|
30
|
+
radius = 'none',
|
|
31
|
+
border = 'none',
|
|
32
|
+
gap,
|
|
33
|
+
padding,
|
|
34
|
+
paddingVertical,
|
|
35
|
+
paddingHorizontal,
|
|
36
|
+
margin,
|
|
37
|
+
marginVertical,
|
|
38
|
+
marginHorizontal,
|
|
39
|
+
backgroundColor: _backgroundColor,
|
|
40
|
+
borderRadius: _borderRadius,
|
|
41
|
+
borderWidth: _borderWidth,
|
|
42
|
+
borderColor: _borderColor,
|
|
43
|
+
style,
|
|
44
|
+
contentContainerStyle,
|
|
45
|
+
showsIndicator = true,
|
|
46
|
+
onScroll,
|
|
47
|
+
onScrollBegin,
|
|
48
|
+
onScrollEnd,
|
|
49
|
+
onEndReached,
|
|
50
|
+
onEndReachedThreshold = 0,
|
|
51
|
+
scrollEnabled = true,
|
|
52
|
+
testID,
|
|
53
|
+
id,
|
|
54
|
+
onLayout,
|
|
55
|
+
}, ref) => {
|
|
56
|
+
const scrollElRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
const isDraggingRef = useRef(false);
|
|
58
|
+
const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
59
|
+
const endReachedRef = useRef(false);
|
|
60
|
+
const layoutRef = useWebLayout<HTMLDivElement>(onLayout);
|
|
61
|
+
|
|
62
|
+
const backgroundVariant = background === 'transparent' ? undefined : background;
|
|
63
|
+
|
|
64
|
+
scrollViewStyles.useVariants({
|
|
65
|
+
background: backgroundVariant,
|
|
66
|
+
radius,
|
|
67
|
+
border,
|
|
68
|
+
gap,
|
|
69
|
+
padding,
|
|
70
|
+
paddingVertical,
|
|
71
|
+
paddingHorizontal,
|
|
72
|
+
margin,
|
|
73
|
+
marginVertical,
|
|
74
|
+
marginHorizontal,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** @ts-ignore */
|
|
78
|
+
const containerWebProps = getWebProps((scrollViewStyles.container as any)({}));
|
|
79
|
+
/** @ts-ignore */
|
|
80
|
+
const scrollRegionWebProps = getWebProps(scrollViewStyles.scrollableRegion);
|
|
81
|
+
/** @ts-ignore */
|
|
82
|
+
const contentWebProps = getWebProps((scrollViewStyles.contentContainer as any)({}));
|
|
83
|
+
|
|
84
|
+
// Imperative handle
|
|
85
|
+
useImperativeHandle(ref, () => ({
|
|
86
|
+
scrollTo: (options) => {
|
|
87
|
+
scrollElRef.current?.scrollTo({
|
|
88
|
+
left: options.x ?? 0,
|
|
89
|
+
top: options.y ?? 0,
|
|
90
|
+
behavior: (options.animated ?? true) ? 'smooth' : 'instant',
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
scrollToEnd: (options) => {
|
|
94
|
+
const el = scrollElRef.current;
|
|
95
|
+
if (!el) return;
|
|
96
|
+
const behavior = (options?.animated ?? true) ? 'smooth' : 'instant';
|
|
97
|
+
if (direction === 'horizontal') {
|
|
98
|
+
el.scrollTo({ left: el.scrollWidth, behavior });
|
|
99
|
+
} else {
|
|
100
|
+
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
scrollToStart: (options) => {
|
|
104
|
+
scrollElRef.current?.scrollTo({
|
|
105
|
+
left: 0,
|
|
106
|
+
top: 0,
|
|
107
|
+
behavior: (options?.animated ?? true) ? 'smooth' : 'instant',
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
getScrollPosition: () => {
|
|
111
|
+
const el = scrollElRef.current;
|
|
112
|
+
return { x: el?.scrollLeft ?? 0, y: el?.scrollTop ?? 0 };
|
|
113
|
+
},
|
|
114
|
+
getInnerElement: () => scrollElRef.current,
|
|
115
|
+
}), [direction]);
|
|
116
|
+
|
|
117
|
+
// Scroll event handling
|
|
118
|
+
const handleScroll = useCallback(() => {
|
|
119
|
+
const el = scrollElRef.current;
|
|
120
|
+
if (!el) return;
|
|
121
|
+
|
|
122
|
+
const event = buildScrollEvent(el);
|
|
123
|
+
onScroll?.(event);
|
|
124
|
+
|
|
125
|
+
// Scroll begin detection (first scroll after idle)
|
|
126
|
+
if (!isDraggingRef.current) {
|
|
127
|
+
isDraggingRef.current = true;
|
|
128
|
+
onScrollBegin?.(event);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Scroll end detection via timeout
|
|
132
|
+
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
|
133
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
134
|
+
isDraggingRef.current = false;
|
|
135
|
+
if (el) onScrollEnd?.(buildScrollEvent(el));
|
|
136
|
+
}, 150);
|
|
137
|
+
|
|
138
|
+
// End reached detection
|
|
139
|
+
if (onEndReached) {
|
|
140
|
+
const distanceFromEnd = direction === 'horizontal'
|
|
141
|
+
? el.scrollWidth - el.clientWidth - el.scrollLeft
|
|
142
|
+
: el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
143
|
+
|
|
144
|
+
if (distanceFromEnd <= onEndReachedThreshold && !endReachedRef.current) {
|
|
145
|
+
endReachedRef.current = true;
|
|
146
|
+
onEndReached();
|
|
147
|
+
} else if (distanceFromEnd > onEndReachedThreshold) {
|
|
148
|
+
endReachedRef.current = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}, [onScroll, onScrollBegin, onScrollEnd, onEndReached, onEndReachedThreshold, direction]);
|
|
152
|
+
|
|
153
|
+
// Attach scroll listener
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const el = scrollElRef.current;
|
|
156
|
+
if (!el) return;
|
|
157
|
+
el.addEventListener('scroll', handleScroll, { passive: true });
|
|
158
|
+
return () => {
|
|
159
|
+
el.removeEventListener('scroll', handleScroll);
|
|
160
|
+
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
|
161
|
+
};
|
|
162
|
+
}, [handleScroll]);
|
|
163
|
+
|
|
164
|
+
const mergedRef = useMergeRefs(layoutRef, containerWebProps.ref);
|
|
165
|
+
|
|
166
|
+
const flatStyle = flattenStyle(style);
|
|
167
|
+
const flatContentStyle = flattenStyle(contentContainerStyle);
|
|
168
|
+
|
|
169
|
+
// Split user styles: sizing/margin to container, visual to content
|
|
170
|
+
const {
|
|
171
|
+
width, height, minWidth, minHeight, maxWidth, maxHeight,
|
|
172
|
+
flex, flexGrow, flexShrink, flexBasis, alignSelf,
|
|
173
|
+
margin: userMargin, marginTop, marginRight, marginBottom, marginLeft,
|
|
174
|
+
marginBlock, marginInline,
|
|
175
|
+
...restStyle
|
|
176
|
+
} = flatStyle;
|
|
177
|
+
|
|
178
|
+
const containerUserStyles: React.CSSProperties = {
|
|
179
|
+
...(width !== undefined && { width }),
|
|
180
|
+
...(height !== undefined && { height }),
|
|
181
|
+
...(minWidth !== undefined && { minWidth }),
|
|
182
|
+
...(minHeight !== undefined && { minHeight }),
|
|
183
|
+
...(maxWidth !== undefined && { maxWidth }),
|
|
184
|
+
...(maxHeight !== undefined && { maxHeight }),
|
|
185
|
+
...(flex !== undefined && { flex }),
|
|
186
|
+
...(flexGrow !== undefined && { flexGrow }),
|
|
187
|
+
...(flexShrink !== undefined && { flexShrink }),
|
|
188
|
+
...(flexBasis !== undefined && { flexBasis }),
|
|
189
|
+
...(alignSelf !== undefined && { alignSelf }),
|
|
190
|
+
...(userMargin !== undefined && { margin: userMargin }),
|
|
191
|
+
...(marginTop !== undefined && { marginTop }),
|
|
192
|
+
...(marginRight !== undefined && { marginRight }),
|
|
193
|
+
...(marginBottom !== undefined && { marginBottom }),
|
|
194
|
+
...(marginLeft !== undefined && { marginLeft }),
|
|
195
|
+
...(marginBlock !== undefined && { marginBlock }),
|
|
196
|
+
...(marginInline !== undefined && { marginInline }),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Determine overflow direction
|
|
200
|
+
const overflowX = direction === 'horizontal' || direction === 'both' ? 'auto' : 'hidden';
|
|
201
|
+
const overflowY = direction === 'vertical' || direction === 'both' ? 'auto' : 'hidden';
|
|
202
|
+
|
|
203
|
+
// Hide scrollbar CSS
|
|
204
|
+
const scrollbarStyle: React.CSSProperties = showsIndicator ? {} : {
|
|
205
|
+
scrollbarWidth: 'none',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
{...containerWebProps}
|
|
211
|
+
style={containerUserStyles}
|
|
212
|
+
ref={mergedRef}
|
|
213
|
+
id={id}
|
|
214
|
+
data-testid={testID}
|
|
215
|
+
>
|
|
216
|
+
<div
|
|
217
|
+
{...scrollRegionWebProps}
|
|
218
|
+
ref={scrollElRef}
|
|
219
|
+
style={{
|
|
220
|
+
position: 'absolute',
|
|
221
|
+
inset: 0,
|
|
222
|
+
overflowX: scrollEnabled ? overflowX : 'hidden',
|
|
223
|
+
overflowY: scrollEnabled ? overflowY : 'hidden',
|
|
224
|
+
boxSizing: 'border-box',
|
|
225
|
+
...scrollbarStyle,
|
|
226
|
+
...restStyle,
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<div
|
|
230
|
+
{...contentWebProps}
|
|
231
|
+
style={flatContentStyle}
|
|
232
|
+
>
|
|
233
|
+
{children}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
ScrollView.displayName = 'ScrollView';
|
|
241
|
+
|
|
242
|
+
export default ScrollView;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Size, Surface } from '@idealyst/theme';
|
|
2
|
+
import type { ReactNode, RefObject } from 'react';
|
|
3
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
4
|
+
import { ContainerStyleProps } from '../utils/viewStyleProps';
|
|
5
|
+
import type { LayoutChangeEvent } from '../hooks/useWebLayout';
|
|
6
|
+
|
|
7
|
+
export type { LayoutChangeEvent };
|
|
8
|
+
|
|
9
|
+
// Component-specific type aliases
|
|
10
|
+
export type ScrollViewBackgroundVariant = Surface | 'transparent';
|
|
11
|
+
export type ScrollViewRadiusVariant = Size | 'none';
|
|
12
|
+
export type ScrollViewBorderVariant = 'none' | 'thin' | 'thick';
|
|
13
|
+
export type ScrollViewDirection = 'vertical' | 'horizontal' | 'both';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scroll position information.
|
|
17
|
+
*/
|
|
18
|
+
export interface ScrollPosition {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Content size information for the scrollable content.
|
|
25
|
+
*/
|
|
26
|
+
export interface ScrollContentSize {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Layout size of the scroll view container.
|
|
33
|
+
*/
|
|
34
|
+
export interface ScrollLayoutSize {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalized scroll event provided to all scroll callbacks.
|
|
41
|
+
*/
|
|
42
|
+
export interface ScrollEvent {
|
|
43
|
+
/** Current scroll offset */
|
|
44
|
+
position: ScrollPosition;
|
|
45
|
+
/** Size of the scrollable content */
|
|
46
|
+
contentSize: ScrollContentSize;
|
|
47
|
+
/** Size of the visible scroll container */
|
|
48
|
+
layoutSize: ScrollLayoutSize;
|
|
49
|
+
/** Whether the content is scrolled to the end (vertical or horizontal depending on direction) */
|
|
50
|
+
isAtEnd: boolean;
|
|
51
|
+
/** Whether the content is scrolled to the start */
|
|
52
|
+
isAtStart: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Options for scrollTo operations.
|
|
57
|
+
*/
|
|
58
|
+
export interface ScrollToOptions {
|
|
59
|
+
x?: number;
|
|
60
|
+
y?: number;
|
|
61
|
+
animated?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Imperative handle exposed via ref for controlling scroll programmatically.
|
|
66
|
+
*/
|
|
67
|
+
export interface ScrollViewRef {
|
|
68
|
+
/** Scroll to a specific position */
|
|
69
|
+
scrollTo: (options: ScrollToOptions) => void;
|
|
70
|
+
/** Scroll to the end of the content */
|
|
71
|
+
scrollToEnd: (options?: { animated?: boolean }) => void;
|
|
72
|
+
/** Scroll to the top/start of the content */
|
|
73
|
+
scrollToStart: (options?: { animated?: boolean }) => void;
|
|
74
|
+
/** Get current scroll position */
|
|
75
|
+
getScrollPosition: () => ScrollPosition;
|
|
76
|
+
/** Get the underlying native element */
|
|
77
|
+
getInnerElement: () => any;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Scrollable container with scroll event abstractions and imperative scroll controls.
|
|
82
|
+
*/
|
|
83
|
+
export interface ScrollViewProps extends ContainerStyleProps {
|
|
84
|
+
children?: ReactNode;
|
|
85
|
+
|
|
86
|
+
/** Scroll direction. Defaults to 'vertical'. */
|
|
87
|
+
direction?: ScrollViewDirection;
|
|
88
|
+
|
|
89
|
+
/** Background variant */
|
|
90
|
+
background?: ScrollViewBackgroundVariant;
|
|
91
|
+
|
|
92
|
+
/** Border radius variant */
|
|
93
|
+
radius?: ScrollViewRadiusVariant;
|
|
94
|
+
|
|
95
|
+
/** Border variant */
|
|
96
|
+
border?: ScrollViewBorderVariant;
|
|
97
|
+
|
|
98
|
+
/** Custom background color (overrides background variant) */
|
|
99
|
+
backgroundColor?: string;
|
|
100
|
+
|
|
101
|
+
/** Custom border radius (overrides radius variant) */
|
|
102
|
+
borderRadius?: number;
|
|
103
|
+
|
|
104
|
+
/** Custom border width (overrides border variant) */
|
|
105
|
+
borderWidth?: number;
|
|
106
|
+
|
|
107
|
+
/** Custom border color */
|
|
108
|
+
borderColor?: string;
|
|
109
|
+
|
|
110
|
+
/** Additional styles */
|
|
111
|
+
style?: StyleProp<ViewStyle>;
|
|
112
|
+
|
|
113
|
+
/** Styles applied to the content container */
|
|
114
|
+
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
115
|
+
|
|
116
|
+
/** Whether to show scroll indicators. Defaults to true. */
|
|
117
|
+
showsIndicator?: boolean;
|
|
118
|
+
|
|
119
|
+
/** Whether to enable paging behavior */
|
|
120
|
+
pagingEnabled?: boolean;
|
|
121
|
+
|
|
122
|
+
/** Whether the scroll view bounces at the edges (iOS). Defaults to true. */
|
|
123
|
+
bounces?: boolean;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Called continuously as the user scrolls.
|
|
127
|
+
* Use `scrollEventThrottle` to control frequency.
|
|
128
|
+
*/
|
|
129
|
+
onScroll?: (event: ScrollEvent) => void;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Called when scrolling begins (user starts dragging).
|
|
133
|
+
*/
|
|
134
|
+
onScrollBegin?: (event: ScrollEvent) => void;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Called when scrolling ends (momentum settles or user lifts finger).
|
|
138
|
+
*/
|
|
139
|
+
onScrollEnd?: (event: ScrollEvent) => void;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Called when the scroll position reaches the end of the content.
|
|
143
|
+
* Useful for infinite scroll / load-more patterns.
|
|
144
|
+
*/
|
|
145
|
+
onEndReached?: () => void;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Distance from the end (in pixels) at which onEndReached fires.
|
|
149
|
+
* Defaults to 0.
|
|
150
|
+
*/
|
|
151
|
+
onEndReachedThreshold?: number;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Throttle interval (ms) for onScroll events on native. Defaults to 16 (~60fps).
|
|
155
|
+
*/
|
|
156
|
+
scrollEventThrottle?: number;
|
|
157
|
+
|
|
158
|
+
/** Whether scrolling is enabled. Defaults to true. */
|
|
159
|
+
scrollEnabled?: boolean;
|
|
160
|
+
|
|
161
|
+
/** Whether the keyboard should dismiss on drag. */
|
|
162
|
+
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
163
|
+
|
|
164
|
+
/** Test ID for testing */
|
|
165
|
+
testID?: string;
|
|
166
|
+
|
|
167
|
+
/** Callback when layout changes */
|
|
168
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
169
|
+
}
|
|
@@ -92,15 +92,15 @@ export const tabBarStyles = defineStyle('TabBar', (theme: Theme) => ({
|
|
|
92
92
|
},
|
|
93
93
|
|
|
94
94
|
tab: ({ type = 'standard', size = 'md', active = false, pillMode: _pillMode = 'light', disabled = false, iconPosition = 'left', justify = 'start' }: TabBarDynamicProps) => {
|
|
95
|
-
// Tab padding for pills
|
|
96
|
-
const
|
|
97
|
-
xs: { paddingVertical: 2, paddingHorizontal:
|
|
98
|
-
sm: { paddingVertical:
|
|
99
|
-
md: { paddingVertical:
|
|
100
|
-
lg: { paddingVertical:
|
|
101
|
-
xl: { paddingVertical:
|
|
95
|
+
// Tab padding for pills — narrower than standard tabs
|
|
96
|
+
const pillsPaddingMap: Record<Size, { paddingVertical: number; paddingHorizontal: number }> = {
|
|
97
|
+
xs: { paddingVertical: 2, paddingHorizontal: 8 },
|
|
98
|
+
sm: { paddingVertical: 3, paddingHorizontal: 10 },
|
|
99
|
+
md: { paddingVertical: 4, paddingHorizontal: 12 },
|
|
100
|
+
lg: { paddingVertical: 6, paddingHorizontal: 16 },
|
|
101
|
+
xl: { paddingVertical: 8, paddingHorizontal: 20 },
|
|
102
102
|
};
|
|
103
|
-
const tabPadding = type === 'pills' ?
|
|
103
|
+
const tabPadding = type === 'pills' ? pillsPaddingMap[size] : {};
|
|
104
104
|
|
|
105
105
|
// Color based on type and active state
|
|
106
106
|
let color = active ? theme.colors.text.primary : theme.colors.text.secondary;
|
|
@@ -127,10 +127,10 @@ export const tabBarStyles = defineStyle('TabBar', (theme: Theme) => ({
|
|
|
127
127
|
variants: {
|
|
128
128
|
size: {
|
|
129
129
|
fontSize: theme.sizes.$tabBar.fontSize,
|
|
130
|
-
paddingHorizontal: theme.sizes.$tabBar.padding,
|
|
131
|
-
paddingTop: theme.sizes.$tabBar.padding,
|
|
132
|
-
paddingBottom: theme.sizes.$tabBar.paddingBottom,
|
|
133
130
|
lineHeight: theme.sizes.$tabBar.lineHeight,
|
|
131
|
+
paddingTop: type === 'pills' ? undefined : theme.sizes.$tabBar.padding,
|
|
132
|
+
paddingBottom: type === 'pills' ? undefined : theme.sizes.$tabBar.paddingBottom,
|
|
133
|
+
paddingHorizontal: type === 'pills' ? undefined : theme.sizes.$tabBar.padding,
|
|
134
134
|
},
|
|
135
135
|
},
|
|
136
136
|
_web: {
|
|
@@ -186,8 +186,8 @@ export const tabBarStyles = defineStyle('TabBar', (theme: Theme) => ({
|
|
|
186
186
|
|
|
187
187
|
const typeStyles = type === 'pills' ? {
|
|
188
188
|
borderRadius: 9999,
|
|
189
|
-
bottom:
|
|
190
|
-
top:
|
|
189
|
+
bottom: 3,
|
|
190
|
+
top: 3,
|
|
191
191
|
left: 0,
|
|
192
192
|
} : {
|
|
193
193
|
bottom: -1,
|
|
@@ -93,9 +93,11 @@ function TH({
|
|
|
93
93
|
{ width, flex: width ? undefined : 1 },
|
|
94
94
|
]}
|
|
95
95
|
>
|
|
96
|
-
|
|
97
|
-
{children}
|
|
98
|
-
|
|
96
|
+
{typeof children === 'string' ? (
|
|
97
|
+
<Text style={headerCellStyle}>{children}</Text>
|
|
98
|
+
) : (
|
|
99
|
+
children
|
|
100
|
+
)}
|
|
99
101
|
</View>
|
|
100
102
|
);
|
|
101
103
|
}
|
|
@@ -131,6 +133,49 @@ function TD({
|
|
|
131
133
|
);
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// TF Component (Footer Cell)
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
interface TFProps {
|
|
141
|
+
children: ReactNode;
|
|
142
|
+
size?: TableSizeVariant;
|
|
143
|
+
type?: TableType;
|
|
144
|
+
align?: TableAlignVariant;
|
|
145
|
+
width?: number | string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function TF({
|
|
149
|
+
children,
|
|
150
|
+
size = 'md',
|
|
151
|
+
type = 'standard',
|
|
152
|
+
align = 'left',
|
|
153
|
+
width,
|
|
154
|
+
}: TFProps) {
|
|
155
|
+
tableStyles.useVariants({
|
|
156
|
+
size,
|
|
157
|
+
type,
|
|
158
|
+
align,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const footerCellStyle = (tableStyles.footerCell as any)({});
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<View
|
|
165
|
+
style={[
|
|
166
|
+
footerCellStyle,
|
|
167
|
+
{ width, flex: width ? undefined : 1 },
|
|
168
|
+
]}
|
|
169
|
+
>
|
|
170
|
+
{typeof children === 'string' ? (
|
|
171
|
+
<Text style={footerCellStyle}>{children}</Text>
|
|
172
|
+
) : (
|
|
173
|
+
children
|
|
174
|
+
)}
|
|
175
|
+
</View>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
134
179
|
// ============================================================================
|
|
135
180
|
// Main Table Component
|
|
136
181
|
// ============================================================================
|
|
@@ -200,6 +245,15 @@ function TableInner<T = any>({
|
|
|
200
245
|
};
|
|
201
246
|
|
|
202
247
|
const isClickable = !!onRowPress;
|
|
248
|
+
const hasFooter = columns.some((col) => col.footer !== undefined);
|
|
249
|
+
|
|
250
|
+
// Helper to resolve footer content
|
|
251
|
+
const getFooterContent = (column: TableColumn<T>) => {
|
|
252
|
+
if (typeof column.footer === 'function') {
|
|
253
|
+
return column.footer(data);
|
|
254
|
+
}
|
|
255
|
+
return column.footer;
|
|
256
|
+
};
|
|
203
257
|
|
|
204
258
|
return (
|
|
205
259
|
<ScrollView
|
|
@@ -253,6 +307,25 @@ function TableInner<T = any>({
|
|
|
253
307
|
</TR>
|
|
254
308
|
))}
|
|
255
309
|
</View>
|
|
310
|
+
|
|
311
|
+
{/* Footer */}
|
|
312
|
+
{hasFooter && (
|
|
313
|
+
<View style={(tableStyles.tfoot as any)({})}>
|
|
314
|
+
<View style={{ flexDirection: 'row' }}>
|
|
315
|
+
{columns.map((column) => (
|
|
316
|
+
<TF
|
|
317
|
+
key={column.key}
|
|
318
|
+
size={size}
|
|
319
|
+
type={type}
|
|
320
|
+
align={column.align}
|
|
321
|
+
width={column.width}
|
|
322
|
+
>
|
|
323
|
+
{getFooterContent(column) ?? null}
|
|
324
|
+
</TF>
|
|
325
|
+
))}
|
|
326
|
+
</View>
|
|
327
|
+
</View>
|
|
328
|
+
)}
|
|
256
329
|
</View>
|
|
257
330
|
</ScrollView>
|
|
258
331
|
);
|
|
@@ -88,6 +88,46 @@ export const tableStyles = defineStyle('Table', (theme: Theme) => ({
|
|
|
88
88
|
|
|
89
89
|
tbody: (_props: TableDynamicProps) => ({}),
|
|
90
90
|
|
|
91
|
+
tfoot: (_props: TableDynamicProps) => ({
|
|
92
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
footerCell: ({ type = 'standard', align = 'left' }: TableDynamicProps) => {
|
|
96
|
+
const alignStyles = {
|
|
97
|
+
left: { textAlign: 'left' as const, justifyContent: 'flex-start' as const },
|
|
98
|
+
center: { textAlign: 'center' as const, justifyContent: 'center' as const },
|
|
99
|
+
right: { textAlign: 'right' as const, justifyContent: 'flex-end' as const },
|
|
100
|
+
}[align];
|
|
101
|
+
|
|
102
|
+
const borderStyles = type === 'bordered' ? {
|
|
103
|
+
borderRightWidth: 1,
|
|
104
|
+
borderRightColor: theme.colors.border.primary,
|
|
105
|
+
} : {};
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
flexDirection: 'row' as const,
|
|
109
|
+
alignItems: 'center' as const,
|
|
110
|
+
fontWeight: '600' as const,
|
|
111
|
+
color: theme.colors.text.primary,
|
|
112
|
+
borderTopWidth: 2,
|
|
113
|
+
borderTopColor: theme.colors.border.primary,
|
|
114
|
+
...alignStyles,
|
|
115
|
+
...borderStyles,
|
|
116
|
+
variants: {
|
|
117
|
+
size: {
|
|
118
|
+
padding: theme.sizes.$table.padding,
|
|
119
|
+
fontSize: theme.sizes.$table.fontSize,
|
|
120
|
+
lineHeight: theme.sizes.$table.lineHeight,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
_web: {
|
|
124
|
+
borderTop: `2px solid ${theme.colors.border.primary}`,
|
|
125
|
+
borderRight: type === 'bordered' ? `1px solid ${theme.colors.border.primary}` : undefined,
|
|
126
|
+
':last-child': type === 'bordered' ? { borderRight: 'none' } : {},
|
|
127
|
+
},
|
|
128
|
+
} as const;
|
|
129
|
+
},
|
|
130
|
+
|
|
91
131
|
row: ({ type = 'standard', clickable = false }: TableDynamicProps) => {
|
|
92
132
|
const typeStyles = type === 'bordered' || type === 'striped' ? {
|
|
93
133
|
borderBottomWidth: 1,
|
package/src/Table/Table.web.tsx
CHANGED
|
@@ -126,6 +126,43 @@ function TD({
|
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// TF Component (Footer Cell)
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
interface TFProps {
|
|
134
|
+
children: ReactNode;
|
|
135
|
+
size?: TableSizeVariant;
|
|
136
|
+
type?: TableType;
|
|
137
|
+
align?: TableAlignVariant;
|
|
138
|
+
width?: number | string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function TF({
|
|
142
|
+
children,
|
|
143
|
+
size = 'md',
|
|
144
|
+
type = 'standard',
|
|
145
|
+
align = 'left',
|
|
146
|
+
width,
|
|
147
|
+
}: TFProps) {
|
|
148
|
+
tableStyles.useVariants({
|
|
149
|
+
size,
|
|
150
|
+
type,
|
|
151
|
+
align,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const footerCellProps = getWebProps([(tableStyles.footerCell as any)({})]);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<td
|
|
158
|
+
{...footerCellProps}
|
|
159
|
+
style={{ width }}
|
|
160
|
+
>
|
|
161
|
+
{children}
|
|
162
|
+
</td>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
129
166
|
// ============================================================================
|
|
130
167
|
// Main Table Component
|
|
131
168
|
// ============================================================================
|
|
@@ -194,6 +231,15 @@ function Table<T = any>({
|
|
|
194
231
|
};
|
|
195
232
|
|
|
196
233
|
const isClickable = !!onRowPress;
|
|
234
|
+
const hasFooter = columns.some((col) => col.footer !== undefined);
|
|
235
|
+
|
|
236
|
+
// Helper to resolve footer content
|
|
237
|
+
const getFooterContent = (column: TableColumn<T>) => {
|
|
238
|
+
if (typeof column.footer === 'function') {
|
|
239
|
+
return column.footer(data);
|
|
240
|
+
}
|
|
241
|
+
return column.footer;
|
|
242
|
+
};
|
|
197
243
|
|
|
198
244
|
return (
|
|
199
245
|
<div {...containerProps} {...ariaProps} id={id} data-testid={testID}>
|
|
@@ -238,6 +284,23 @@ function Table<T = any>({
|
|
|
238
284
|
</TR>
|
|
239
285
|
))}
|
|
240
286
|
</tbody>
|
|
287
|
+
{hasFooter && (
|
|
288
|
+
<tfoot {...getWebProps([(tableStyles.tfoot as any)({})])}>
|
|
289
|
+
<tr>
|
|
290
|
+
{columns.map((column) => (
|
|
291
|
+
<TF
|
|
292
|
+
key={column.key}
|
|
293
|
+
size={size}
|
|
294
|
+
type={type}
|
|
295
|
+
align={column.align}
|
|
296
|
+
width={column.width}
|
|
297
|
+
>
|
|
298
|
+
{getFooterContent(column) ?? null}
|
|
299
|
+
</TF>
|
|
300
|
+
))}
|
|
301
|
+
</tr>
|
|
302
|
+
</tfoot>
|
|
303
|
+
)}
|
|
241
304
|
</table>
|
|
242
305
|
</div>
|
|
243
306
|
);
|
package/src/Table/types.ts
CHANGED
|
@@ -11,9 +11,10 @@ export type TableAlignVariant = 'left' | 'center' | 'right';
|
|
|
11
11
|
|
|
12
12
|
export interface TableColumn<T = any> extends SortableAccessibilityProps {
|
|
13
13
|
key: string;
|
|
14
|
-
title:
|
|
14
|
+
title: ReactNode;
|
|
15
15
|
dataIndex?: string;
|
|
16
16
|
render?: (value: any, row: T, index: number) => ReactNode;
|
|
17
|
+
footer?: ReactNode | ((data: T[]) => ReactNode);
|
|
17
18
|
width?: number | string;
|
|
18
19
|
align?: TableAlignVariant;
|
|
19
20
|
}
|
package/src/View/types.ts
CHANGED
|
@@ -90,6 +90,10 @@ export interface ViewProps extends ContainerStyleProps {
|
|
|
90
90
|
style?: ViewStyleProp;
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
+
* @deprecated Use the ScrollView component instead, which provides scroll event
|
|
94
|
+
* abstractions (onScroll, onScrollBegin, onScrollEnd, onEndReached) and imperative
|
|
95
|
+
* scroll controls (scrollTo, scrollToEnd, scrollToStart) via ref.
|
|
96
|
+
*
|
|
93
97
|
* Enable scrollable content.
|
|
94
98
|
* - Native: Wraps children in a ScrollView
|
|
95
99
|
* - Web: Renders a wrapper + content structure where the wrapper fills available
|
package/src/index.native.ts
CHANGED
|
@@ -10,6 +10,9 @@ export * from './Text/types';
|
|
|
10
10
|
export { default as View } from './View';
|
|
11
11
|
export * from './View/types';
|
|
12
12
|
|
|
13
|
+
export { default as ScrollView } from './ScrollView';
|
|
14
|
+
export * from './ScrollView/types';
|
|
15
|
+
|
|
13
16
|
export { default as Pressable } from './Pressable';
|
|
14
17
|
export * from './Pressable/types';
|
|
15
18
|
|
|
@@ -126,6 +129,7 @@ export type { ButtonProps } from './Button/types';
|
|
|
126
129
|
export type { IconButtonProps } from './IconButton/types';
|
|
127
130
|
export type { TextProps } from './Text/types';
|
|
128
131
|
export type { ViewProps } from './View/types';
|
|
132
|
+
export type { ScrollViewProps, ScrollViewRef, ScrollEvent, ScrollPosition, ScrollToOptions } from './ScrollView/types';
|
|
129
133
|
export type { InputProps } from './Input/types';
|
|
130
134
|
export type { TextInputProps } from './TextInput/types';
|
|
131
135
|
export type { CheckboxProps } from './Checkbox/types';
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ export * from './Text/types';
|
|
|
15
15
|
export { default as View } from './View';
|
|
16
16
|
export * from './View/types';
|
|
17
17
|
|
|
18
|
+
export { default as ScrollView } from './ScrollView';
|
|
19
|
+
export * from './ScrollView/types';
|
|
20
|
+
|
|
18
21
|
export { default as Pressable } from './Pressable';
|
|
19
22
|
export * from './Pressable/types';
|
|
20
23
|
|
|
@@ -134,6 +137,7 @@ export type { ButtonProps } from './Button/types';
|
|
|
134
137
|
export type { IconButtonProps } from './IconButton/types';
|
|
135
138
|
export type { TextProps } from './Text/types';
|
|
136
139
|
export type { ViewProps } from './View/types';
|
|
140
|
+
export type { ScrollViewProps, ScrollViewRef, ScrollEvent, ScrollPosition, ScrollToOptions } from './ScrollView/types';
|
|
137
141
|
export type { LinkProps } from './Link/types';
|
|
138
142
|
export type { TextInputProps } from './TextInput/types';
|
|
139
143
|
/** @deprecated Use TextInputProps instead */
|