@idealyst/components 1.3.3 → 1.3.5
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/Breadcrumb/Breadcrumb.native.tsx +16 -34
- package/src/Breadcrumb/Breadcrumb.styles.tsx +63 -56
- package/src/Breadcrumb/Breadcrumb.web.tsx +47 -57
- package/src/Menu/Menu.styles.tsx +3 -3
- package/src/Menu/MenuItem.styles.tsx +6 -3
- package/src/Menu/MenuItem.web.tsx +1 -12
- package/src/Table/Table.native.tsx +90 -9
- package/src/Table/Table.styles.tsx +61 -0
- package/src/Table/Table.web.tsx +94 -6
- package/src/Table/types.ts +17 -0
- package/src/examples/TableExamples.tsx +95 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
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.3.
|
|
59
|
+
"@idealyst/theme": "^1.3.5",
|
|
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.3.
|
|
115
|
-
"@idealyst/tooling": "^1.3.
|
|
114
|
+
"@idealyst/theme": "^1.3.5",
|
|
115
|
+
"@idealyst/tooling": "^1.3.5",
|
|
116
116
|
"@mdi/react": "^1.6.1",
|
|
117
117
|
"@types/react": "^19.1.0",
|
|
118
118
|
"react": "^19.1.0",
|
|
@@ -17,34 +17,26 @@ interface BreadcrumbItemProps {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, intent, itemStyle }) => {
|
|
20
|
-
const isClickable = !!item.onPress && !item.disabled;
|
|
21
20
|
const isDisabled = item.disabled || false;
|
|
22
21
|
|
|
23
|
-
// Apply size variant
|
|
24
22
|
breadcrumbStyles.useVariants({
|
|
25
23
|
size,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Get dynamic item text style
|
|
29
|
-
const itemTextStyle = (breadcrumbStyles.itemText as any)({
|
|
30
24
|
intent,
|
|
31
|
-
isLast,
|
|
25
|
+
active: isLast,
|
|
32
26
|
disabled: isDisabled,
|
|
33
|
-
clickable: isClickable,
|
|
34
27
|
});
|
|
35
28
|
|
|
36
|
-
const
|
|
29
|
+
const iconSize = (breadcrumbStyles.icon as any).width || 16;
|
|
37
30
|
|
|
38
31
|
const renderIcon = () => {
|
|
39
32
|
if (!item.icon) return null;
|
|
40
33
|
|
|
41
34
|
if (typeof item.icon === 'string') {
|
|
42
|
-
const iconSize = iconStyle.width || 16;
|
|
43
35
|
return (
|
|
44
36
|
<Icon
|
|
45
37
|
name={item.icon as IconName}
|
|
46
38
|
size={iconSize}
|
|
47
|
-
style={
|
|
39
|
+
style={breadcrumbStyles.icon}
|
|
48
40
|
/>
|
|
49
41
|
);
|
|
50
42
|
} else if (isValidElement(item.icon)) {
|
|
@@ -54,16 +46,14 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
|
|
|
54
46
|
return null;
|
|
55
47
|
};
|
|
56
48
|
|
|
57
|
-
const itemContainerStyle = (breadcrumbStyles.item as any)({});
|
|
58
|
-
|
|
59
49
|
const content = (
|
|
60
|
-
<View style={[
|
|
61
|
-
{item.icon && <View style={
|
|
62
|
-
<Text style={
|
|
50
|
+
<View style={[breadcrumbStyles.item, itemStyle]}>
|
|
51
|
+
{item.icon && <View style={breadcrumbStyles.icon}>{renderIcon()}</View>}
|
|
52
|
+
<Text style={breadcrumbStyles.itemText}>{item.label}</Text>
|
|
63
53
|
</View>
|
|
64
54
|
);
|
|
65
55
|
|
|
66
|
-
if (
|
|
56
|
+
if (!!item.onPress && !item.disabled) {
|
|
67
57
|
return (
|
|
68
58
|
<Pressable
|
|
69
59
|
onPress={item.onPress}
|
|
@@ -91,12 +81,10 @@ const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = ({ separator, si
|
|
|
91
81
|
breadcrumbStyles.useVariants({ size });
|
|
92
82
|
|
|
93
83
|
if (typeof separator === 'string') {
|
|
94
|
-
|
|
95
|
-
return <Text style={[sepStyle, separatorStyle]}>{separator}</Text>;
|
|
84
|
+
return <Text style={[breadcrumbStyles.separator, separatorStyle]}>{separator}</Text>;
|
|
96
85
|
}
|
|
97
86
|
|
|
98
|
-
|
|
99
|
-
return <View style={[sepIconStyle, separatorStyle]}>{separator}</View>;
|
|
87
|
+
return <View style={[breadcrumbStyles.separatorIcon, separatorStyle]}>{separator}</View>;
|
|
100
88
|
};
|
|
101
89
|
|
|
102
90
|
interface BreadcrumbEllipsisProps {
|
|
@@ -105,13 +93,11 @@ interface BreadcrumbEllipsisProps {
|
|
|
105
93
|
}
|
|
106
94
|
|
|
107
95
|
const BreadcrumbEllipsis: React.FC<BreadcrumbEllipsisProps> = ({ size, intent }) => {
|
|
108
|
-
breadcrumbStyles.useVariants({ size });
|
|
109
|
-
const ellipsisStyle = (breadcrumbStyles.ellipsis as any)({});
|
|
110
|
-
const iconStyle = (breadcrumbStyles.ellipsisIcon as any)({ intent });
|
|
96
|
+
breadcrumbStyles.useVariants({ size, intent });
|
|
111
97
|
|
|
112
98
|
return (
|
|
113
|
-
<View style={
|
|
114
|
-
<Icon name="dots-horizontal" style={
|
|
99
|
+
<View style={breadcrumbStyles.ellipsis}>
|
|
100
|
+
<Icon name="dots-horizontal" style={breadcrumbStyles.ellipsisIcon} />
|
|
115
101
|
</View>
|
|
116
102
|
);
|
|
117
103
|
};
|
|
@@ -132,11 +118,7 @@ const Breadcrumb = forwardRef<IdealystElement, BreadcrumbProps>(({
|
|
|
132
118
|
}, ref) => {
|
|
133
119
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
134
120
|
|
|
135
|
-
|
|
136
|
-
breadcrumbStyles.useVariants({ size });
|
|
137
|
-
const containerStyle = (breadcrumbStyles.container as any)({});
|
|
138
|
-
const menuButtonStyle = (breadcrumbStyles.menuButton as any)({});
|
|
139
|
-
const menuIconStyle = (breadcrumbStyles.menuButtonIcon as any)({ intent });
|
|
121
|
+
breadcrumbStyles.useVariants({ size, intent });
|
|
140
122
|
|
|
141
123
|
// Handle responsive collapsing
|
|
142
124
|
let displayItems = items;
|
|
@@ -174,7 +156,7 @@ const Breadcrumb = forwardRef<IdealystElement, BreadcrumbProps>(({
|
|
|
174
156
|
<View
|
|
175
157
|
ref={ref as any}
|
|
176
158
|
nativeID={id}
|
|
177
|
-
style={[
|
|
159
|
+
style={[breadcrumbStyles.container, style]}
|
|
178
160
|
testID={testID}
|
|
179
161
|
accessibilityLabel="Breadcrumb"
|
|
180
162
|
>
|
|
@@ -202,11 +184,11 @@ const Breadcrumb = forwardRef<IdealystElement, BreadcrumbProps>(({
|
|
|
202
184
|
size={size}
|
|
203
185
|
>
|
|
204
186
|
<Pressable
|
|
205
|
-
style={
|
|
187
|
+
style={breadcrumbStyles.menuButton}
|
|
206
188
|
accessibilityRole="button"
|
|
207
189
|
accessibilityLabel="Show more breadcrumb items"
|
|
208
190
|
>
|
|
209
|
-
<Icon name="dots-horizontal" style={
|
|
191
|
+
<Icon name="dots-horizontal" style={breadcrumbStyles.menuButtonIcon} />
|
|
210
192
|
</Pressable>
|
|
211
193
|
</Menu>
|
|
212
194
|
<BreadcrumbSeparator separator={separator} size={size} separatorStyle={separatorStyle} />
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Breadcrumb styles using defineStyle with
|
|
2
|
+
* Breadcrumb styles using defineStyle with static variants.
|
|
3
3
|
*/
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
5
|
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
6
|
-
import type { Theme as BaseTheme
|
|
6
|
+
import type { Theme as BaseTheme } from '@idealyst/theme';
|
|
7
|
+
import { ViewStyleSize } from '../utils/viewStyleProps';
|
|
7
8
|
|
|
8
9
|
// Required: Unistyles must see StyleSheet usage in original source to process this file
|
|
9
10
|
void StyleSheet;
|
|
@@ -11,65 +12,61 @@ void StyleSheet;
|
|
|
11
12
|
// Wrap theme for $iterator support
|
|
12
13
|
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
13
14
|
|
|
14
|
-
type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
intent?: BreadcrumbIntent;
|
|
15
|
+
export type BreadcrumbVariants = {
|
|
16
|
+
size?: ViewStyleSize;
|
|
17
|
+
intent?: 'primary' | 'neutral';
|
|
18
|
+
active?: boolean;
|
|
19
19
|
disabled?: boolean;
|
|
20
|
-
isLast?: boolean;
|
|
21
|
-
clickable?: boolean;
|
|
22
20
|
};
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
|
-
* Breadcrumb styles with
|
|
23
|
+
* Breadcrumb styles with static variants.
|
|
26
24
|
*/
|
|
27
25
|
export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
|
|
28
|
-
container:
|
|
26
|
+
container: {
|
|
29
27
|
display: 'flex' as const,
|
|
30
28
|
flexDirection: 'row' as const,
|
|
31
29
|
alignItems: 'center' as const,
|
|
32
30
|
flexWrap: 'wrap' as const,
|
|
33
31
|
gap: 4,
|
|
34
|
-
}
|
|
32
|
+
},
|
|
35
33
|
|
|
36
|
-
item:
|
|
34
|
+
item: {
|
|
37
35
|
display: 'flex' as const,
|
|
38
36
|
flexDirection: 'row' as const,
|
|
39
37
|
alignItems: 'center' as const,
|
|
40
38
|
gap: 4,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
:
|
|
48
|
-
|
|
49
|
-
:
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
opacity: 0.7,
|
|
40
|
+
variants: {
|
|
41
|
+
active: {
|
|
42
|
+
true: { opacity: 1 },
|
|
43
|
+
false: { _web: { _hover: { opacity: 1 } } },
|
|
44
|
+
},
|
|
45
|
+
disabled: {
|
|
46
|
+
true: { opacity: 0.5 },
|
|
47
|
+
false: {},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
_web: {
|
|
51
|
+
transition: 'opacity 0.2s ease',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
},
|
|
55
|
+
itemText: {
|
|
56
|
+
color: theme.colors.text.primary,
|
|
57
|
+
variants: {
|
|
58
|
+
size: {
|
|
59
|
+
fontSize: theme.sizes.$breadcrumb.fontSize,
|
|
60
|
+
lineHeight: theme.sizes.$breadcrumb.lineHeight,
|
|
61
|
+
},
|
|
62
|
+
disabled: {
|
|
63
|
+
true: { color: theme.colors.text.secondary },
|
|
64
|
+
false: {},
|
|
62
65
|
},
|
|
63
|
-
|
|
64
|
-
_hover: {
|
|
65
|
-
textDecoration: 'underline',
|
|
66
|
-
opacity: 0.8,
|
|
67
|
-
},
|
|
68
|
-
} : {},
|
|
69
|
-
} as const;
|
|
66
|
+
},
|
|
70
67
|
},
|
|
71
68
|
|
|
72
|
-
icon:
|
|
69
|
+
icon: {
|
|
73
70
|
variants: {
|
|
74
71
|
size: {
|
|
75
72
|
width: theme.sizes.$breadcrumb.iconSize,
|
|
@@ -77,23 +74,25 @@ export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
|
|
|
77
74
|
fontSize: theme.sizes.$breadcrumb.iconSize,
|
|
78
75
|
},
|
|
79
76
|
},
|
|
80
|
-
}
|
|
77
|
+
},
|
|
81
78
|
|
|
82
|
-
separator:
|
|
79
|
+
separator: {
|
|
83
80
|
color: theme.colors.text.tertiary,
|
|
81
|
+
opacity: 0.9,
|
|
84
82
|
variants: {
|
|
85
83
|
size: {
|
|
86
84
|
fontSize: theme.sizes.$breadcrumb.fontSize,
|
|
87
85
|
lineHeight: theme.sizes.$breadcrumb.lineHeight,
|
|
88
86
|
},
|
|
89
87
|
},
|
|
90
|
-
}
|
|
88
|
+
},
|
|
91
89
|
|
|
92
|
-
separatorIcon:
|
|
90
|
+
separatorIcon: {
|
|
93
91
|
display: 'flex' as const,
|
|
94
92
|
alignItems: 'center' as const,
|
|
95
93
|
justifyContent: 'center' as const,
|
|
96
94
|
color: theme.colors.text.tertiary,
|
|
95
|
+
opacity: 0.9,
|
|
97
96
|
variants: {
|
|
98
97
|
size: {
|
|
99
98
|
width: theme.sizes.$breadcrumb.iconSize,
|
|
@@ -101,38 +100,46 @@ export const breadcrumbStyles = defineStyle('Breadcrumb', (theme: Theme) => ({
|
|
|
101
100
|
fontSize: theme.sizes.$breadcrumb.iconSize,
|
|
102
101
|
},
|
|
103
102
|
},
|
|
104
|
-
}
|
|
103
|
+
},
|
|
105
104
|
|
|
106
|
-
ellipsis:
|
|
105
|
+
ellipsis: {
|
|
107
106
|
display: 'flex' as const,
|
|
108
107
|
alignItems: 'center' as const,
|
|
109
108
|
justifyContent: 'center' as const,
|
|
110
|
-
}
|
|
109
|
+
},
|
|
111
110
|
|
|
112
|
-
ellipsisIcon:
|
|
113
|
-
color:
|
|
111
|
+
ellipsisIcon: {
|
|
112
|
+
color: theme.colors.text.secondary,
|
|
114
113
|
variants: {
|
|
115
114
|
size: {
|
|
116
115
|
width: theme.sizes.$breadcrumb.iconSize,
|
|
117
116
|
height: theme.sizes.$breadcrumb.iconSize,
|
|
118
117
|
fontSize: theme.sizes.$breadcrumb.iconSize,
|
|
119
118
|
},
|
|
119
|
+
intent: {
|
|
120
|
+
primary: { color: theme.intents.primary.primary },
|
|
121
|
+
neutral: { color: theme.colors.text.secondary },
|
|
122
|
+
},
|
|
120
123
|
},
|
|
121
|
-
}
|
|
124
|
+
},
|
|
122
125
|
|
|
123
|
-
menuButton:
|
|
126
|
+
menuButton: {
|
|
124
127
|
paddingVertical: 4,
|
|
125
128
|
paddingHorizontal: 8,
|
|
126
|
-
}
|
|
129
|
+
},
|
|
127
130
|
|
|
128
|
-
menuButtonIcon:
|
|
129
|
-
color:
|
|
131
|
+
menuButtonIcon: {
|
|
132
|
+
color: theme.colors.text.secondary,
|
|
130
133
|
variants: {
|
|
131
134
|
size: {
|
|
132
135
|
width: theme.sizes.$breadcrumb.iconSize,
|
|
133
136
|
height: theme.sizes.$breadcrumb.iconSize,
|
|
134
137
|
fontSize: theme.sizes.$breadcrumb.iconSize,
|
|
135
138
|
},
|
|
139
|
+
intent: {
|
|
140
|
+
primary: { color: theme.intents.primary.primary },
|
|
141
|
+
neutral: { color: theme.colors.text.secondary },
|
|
142
|
+
},
|
|
136
143
|
},
|
|
137
|
-
}
|
|
144
|
+
},
|
|
138
145
|
}));
|
|
@@ -19,24 +19,16 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
|
|
|
19
19
|
const isClickable = !!item.onPress && !item.disabled;
|
|
20
20
|
const isDisabled = item.disabled || false;
|
|
21
21
|
|
|
22
|
-
// Apply size variant
|
|
23
22
|
breadcrumbStyles.useVariants({
|
|
24
23
|
size,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Get dynamic styles - call as functions for theme reactivity
|
|
28
|
-
const itemStyle_ = (breadcrumbStyles.item as any)({});
|
|
29
|
-
const itemTextStyle = (breadcrumbStyles.itemText as any)({
|
|
30
24
|
intent,
|
|
31
|
-
isLast,
|
|
25
|
+
active: isLast,
|
|
32
26
|
disabled: isDisabled,
|
|
33
|
-
clickable: isClickable,
|
|
34
27
|
});
|
|
35
|
-
const iconStyle = (breadcrumbStyles.icon as any)({});
|
|
36
28
|
|
|
37
|
-
const itemProps = getWebProps([
|
|
38
|
-
const itemTextProps = getWebProps([
|
|
39
|
-
const iconProps = getWebProps([
|
|
29
|
+
const itemProps = getWebProps([breadcrumbStyles.item]);
|
|
30
|
+
const itemTextProps = getWebProps([breadcrumbStyles.itemText, itemStyle]);
|
|
31
|
+
const iconProps = getWebProps([breadcrumbStyles.icon]);
|
|
40
32
|
|
|
41
33
|
const handleClick = () => {
|
|
42
34
|
if (!item.disabled && item.onPress) {
|
|
@@ -61,30 +53,13 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
|
|
|
61
53
|
return null;
|
|
62
54
|
};
|
|
63
55
|
|
|
64
|
-
const content = (
|
|
65
|
-
<div {...itemProps}>
|
|
66
|
-
{item.icon && (
|
|
67
|
-
<span
|
|
68
|
-
{...iconProps}
|
|
69
|
-
style={{
|
|
70
|
-
display: 'inline-flex',
|
|
71
|
-
alignItems: 'center',
|
|
72
|
-
justifyContent: 'center',
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
{renderIcon()}
|
|
76
|
-
</span>
|
|
77
|
-
)}
|
|
78
|
-
<span {...itemTextProps}>
|
|
79
|
-
{item.label}
|
|
80
|
-
</span>
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
|
|
84
56
|
if (isClickable) {
|
|
85
57
|
return (
|
|
86
58
|
<button
|
|
59
|
+
{...itemProps}
|
|
87
60
|
onClick={handleClick}
|
|
61
|
+
disabled={isDisabled}
|
|
62
|
+
aria-current={isLast ? 'page' : undefined}
|
|
88
63
|
style={{
|
|
89
64
|
background: 'none',
|
|
90
65
|
border: 'none',
|
|
@@ -95,17 +70,43 @@ const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ item, isLast, size, int
|
|
|
95
70
|
font: 'inherit',
|
|
96
71
|
color: 'inherit',
|
|
97
72
|
}}
|
|
98
|
-
disabled={isDisabled}
|
|
99
|
-
aria-current={isLast ? 'page' : undefined}
|
|
100
73
|
>
|
|
101
|
-
{
|
|
74
|
+
{item.icon && (
|
|
75
|
+
<span
|
|
76
|
+
{...iconProps}
|
|
77
|
+
style={{
|
|
78
|
+
display: 'inline-flex',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
justifyContent: 'center',
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{renderIcon()}
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
<span {...itemTextProps}>
|
|
87
|
+
{item.label}
|
|
88
|
+
</span>
|
|
102
89
|
</button>
|
|
103
90
|
);
|
|
104
91
|
}
|
|
105
92
|
|
|
106
93
|
return (
|
|
107
|
-
<div aria-current={isLast ? 'page' : undefined}>
|
|
108
|
-
{
|
|
94
|
+
<div {...itemProps} aria-current={isLast ? 'page' : undefined}>
|
|
95
|
+
{item.icon && (
|
|
96
|
+
<span
|
|
97
|
+
{...iconProps}
|
|
98
|
+
style={{
|
|
99
|
+
display: 'inline-flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{renderIcon()}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
<span {...itemTextProps}>
|
|
108
|
+
{item.label}
|
|
109
|
+
</span>
|
|
109
110
|
</div>
|
|
110
111
|
);
|
|
111
112
|
};
|
|
@@ -121,8 +122,7 @@ const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = ({ separator, si
|
|
|
121
122
|
const isTextSeparator = typeof separator === 'string';
|
|
122
123
|
|
|
123
124
|
if (isTextSeparator) {
|
|
124
|
-
const
|
|
125
|
-
const separatorProps = getWebProps([separatorStyle_, separatorStyle]);
|
|
125
|
+
const separatorProps = getWebProps([breadcrumbStyles.separator, separatorStyle]);
|
|
126
126
|
return (
|
|
127
127
|
<span {...separatorProps} aria-hidden="true">
|
|
128
128
|
{separator}
|
|
@@ -130,8 +130,7 @@ const BreadcrumbSeparator: React.FC<BreadcrumbSeparatorProps> = ({ separator, si
|
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const
|
|
134
|
-
const separatorIconProps = getWebProps([separatorIconStyle, separatorStyle]);
|
|
133
|
+
const separatorIconProps = getWebProps([breadcrumbStyles.separatorIcon, separatorStyle]);
|
|
135
134
|
return (
|
|
136
135
|
<span {...separatorIconProps} aria-hidden="true">
|
|
137
136
|
{separator}
|
|
@@ -145,11 +144,9 @@ interface BreadcrumbEllipsisProps {
|
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
const BreadcrumbEllipsis: React.FC<BreadcrumbEllipsisProps> = ({ size, intent }) => {
|
|
148
|
-
breadcrumbStyles.useVariants({ size });
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
const ellipsisProps = getWebProps([ellipsisStyle]);
|
|
152
|
-
const iconProps = getWebProps([ellipsisIconStyle]);
|
|
147
|
+
breadcrumbStyles.useVariants({ size, intent });
|
|
148
|
+
const ellipsisProps = getWebProps([breadcrumbStyles.ellipsis]);
|
|
149
|
+
const iconProps = getWebProps([breadcrumbStyles.ellipsisIcon]);
|
|
153
150
|
|
|
154
151
|
return (
|
|
155
152
|
<span {...ellipsisProps}>
|
|
@@ -182,17 +179,10 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({
|
|
|
182
179
|
}) => {
|
|
183
180
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
184
181
|
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const containerProps = getWebProps([containerStyle, style as any]);
|
|
191
|
-
|
|
192
|
-
// Apply variants for menu button
|
|
193
|
-
breadcrumbStyles.useVariants({ size });
|
|
194
|
-
const menuButtonProps = getWebProps([menuButtonStyle]);
|
|
195
|
-
const menuIconProps = getWebProps([menuButtonIconStyle]);
|
|
182
|
+
breadcrumbStyles.useVariants({ size, intent });
|
|
183
|
+
const containerProps = getWebProps([breadcrumbStyles.container, style as any]);
|
|
184
|
+
const menuButtonProps = getWebProps([breadcrumbStyles.menuButton]);
|
|
185
|
+
const menuIconProps = getWebProps([breadcrumbStyles.menuButtonIcon]);
|
|
196
186
|
|
|
197
187
|
// Handle responsive collapsing
|
|
198
188
|
let displayItems = items;
|
package/src/Menu/Menu.styles.tsx
CHANGED
|
@@ -57,12 +57,12 @@ export const menuStyles = defineStyle('Menu', (theme: Theme) => ({
|
|
|
57
57
|
flexDirection: 'row' as const,
|
|
58
58
|
alignItems: 'center' as const,
|
|
59
59
|
backgroundColor: 'transparent' as const,
|
|
60
|
-
borderRadius: 4,
|
|
61
|
-
minHeight: 44,
|
|
62
60
|
variants: {
|
|
63
61
|
size: {
|
|
64
62
|
paddingVertical: theme.sizes.$menu.paddingVertical,
|
|
65
63
|
paddingHorizontal: theme.sizes.$menu.paddingHorizontal,
|
|
64
|
+
minHeight: theme.sizes.$menu.minHeight,
|
|
65
|
+
borderRadius: theme.sizes.$menu.borderRadius,
|
|
66
66
|
},
|
|
67
67
|
intent: {
|
|
68
68
|
neutral: {
|
|
@@ -147,13 +147,13 @@ export const menuStyles = defineStyle('Menu', (theme: Theme) => ({
|
|
|
147
147
|
alignItems: 'center' as const,
|
|
148
148
|
justifyContent: 'center' as const,
|
|
149
149
|
flexShrink: 0,
|
|
150
|
-
marginRight: 8,
|
|
151
150
|
color: theme.colors.text.primary,
|
|
152
151
|
variants: {
|
|
153
152
|
size: {
|
|
154
153
|
width: theme.sizes.$menu.iconSize,
|
|
155
154
|
height: theme.sizes.$menu.iconSize,
|
|
156
155
|
fontSize: theme.sizes.$menu.iconSize,
|
|
156
|
+
marginRight: theme.sizes.$menu.iconGap,
|
|
157
157
|
},
|
|
158
158
|
intent: {
|
|
159
159
|
neutral: {},
|
|
@@ -25,12 +25,12 @@ export const menuItemStyles = defineStyle('MenuItem', (theme: Theme) => ({
|
|
|
25
25
|
flexDirection: 'row' as const,
|
|
26
26
|
alignItems: 'center' as const,
|
|
27
27
|
backgroundColor: 'transparent' as const,
|
|
28
|
-
borderRadius: 4,
|
|
29
|
-
minHeight: 44,
|
|
30
28
|
variants: {
|
|
31
29
|
size: {
|
|
32
30
|
paddingVertical: theme.sizes.$menu.paddingVertical,
|
|
33
31
|
paddingHorizontal: theme.sizes.$menu.paddingHorizontal,
|
|
32
|
+
minHeight: theme.sizes.$menu.minHeight,
|
|
33
|
+
borderRadius: theme.sizes.$menu.borderRadius,
|
|
34
34
|
},
|
|
35
35
|
intent: {
|
|
36
36
|
neutral: {
|
|
@@ -110,13 +110,13 @@ export const menuItemStyles = defineStyle('MenuItem', (theme: Theme) => ({
|
|
|
110
110
|
alignItems: 'center' as const,
|
|
111
111
|
justifyContent: 'center' as const,
|
|
112
112
|
flexShrink: 0,
|
|
113
|
-
marginRight: 12,
|
|
114
113
|
color: theme.colors.text.primary,
|
|
115
114
|
variants: {
|
|
116
115
|
size: {
|
|
117
116
|
width: theme.sizes.$menu.iconSize,
|
|
118
117
|
height: theme.sizes.$menu.iconSize,
|
|
119
118
|
fontSize: theme.sizes.$menu.iconSize,
|
|
119
|
+
marginRight: theme.sizes.$menu.iconGap,
|
|
120
120
|
},
|
|
121
121
|
intent: {
|
|
122
122
|
neutral: {},
|
|
@@ -135,6 +135,9 @@ export const menuItemStyles = defineStyle('MenuItem', (theme: Theme) => ({
|
|
|
135
135
|
label: (_props: MenuItemDynamicProps) => ({
|
|
136
136
|
flex: 1,
|
|
137
137
|
color: theme.colors.text.primary,
|
|
138
|
+
_web: {
|
|
139
|
+
whiteSpace: 'nowrap',
|
|
140
|
+
},
|
|
138
141
|
variants: {
|
|
139
142
|
size: {
|
|
140
143
|
fontSize: theme.sizes.$menu.labelFontSize,
|
|
@@ -18,6 +18,7 @@ const MenuItem = forwardRef<IdealystElement, MenuItemProps>(({ item, onPress, si
|
|
|
18
18
|
// Initialize styles with useVariants (for size and disabled)
|
|
19
19
|
menuItemStyles.useVariants({
|
|
20
20
|
size,
|
|
21
|
+
intent: item.intent || 'neutral',
|
|
21
22
|
disabled: Boolean(item.disabled),
|
|
22
23
|
});
|
|
23
24
|
|
|
@@ -54,22 +55,10 @@ const MenuItem = forwardRef<IdealystElement, MenuItemProps>(({ item, onPress, si
|
|
|
54
55
|
// Merge refs
|
|
55
56
|
const mergedRef = useMergeRefs(ref, itemProps.ref);
|
|
56
57
|
|
|
57
|
-
// Button reset styles that must be applied directly
|
|
58
|
-
const buttonResetStyles: React.CSSProperties = {
|
|
59
|
-
display: 'flex',
|
|
60
|
-
width: '100%',
|
|
61
|
-
border: 'none',
|
|
62
|
-
outline: 'none',
|
|
63
|
-
cursor: item.disabled ? 'not-allowed' : 'pointer',
|
|
64
|
-
background: 'transparent',
|
|
65
|
-
textAlign: 'left',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
58
|
return (
|
|
69
59
|
<button
|
|
70
60
|
{...itemProps}
|
|
71
61
|
ref={mergedRef}
|
|
72
|
-
style={buttonResetStyles}
|
|
73
62
|
onClick={(e: React.MouseEvent) => {
|
|
74
63
|
e.preventDefault();
|
|
75
64
|
e.stopPropagation();
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import React, { forwardRef, useMemo, ReactNode } from 'react';
|
|
2
|
-
import { View, ScrollView, Text, TouchableOpacity } from 'react-native';
|
|
1
|
+
import React, { forwardRef, useMemo, useState, useCallback, ReactNode } from 'react';
|
|
2
|
+
import { View, ScrollView, Text, TouchableOpacity, Pressable } from 'react-native';
|
|
3
3
|
import { tableStyles } from './Table.styles';
|
|
4
|
-
import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant } from './types';
|
|
4
|
+
import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant, SortDirection } from './types';
|
|
5
|
+
import type { MenuItem } from '../Menu/types';
|
|
5
6
|
import { getNativeAccessibilityProps } from '../utils/accessibility';
|
|
7
|
+
import Icon from '../Icon/Icon.native';
|
|
8
|
+
import Menu from '../Menu/Menu.native';
|
|
6
9
|
|
|
7
10
|
// ============================================================================
|
|
8
11
|
// Sub-component Props
|
|
@@ -25,6 +28,10 @@ interface THProps {
|
|
|
25
28
|
type?: TableType;
|
|
26
29
|
align?: TableAlignVariant;
|
|
27
30
|
width?: number | string;
|
|
31
|
+
sortable?: boolean;
|
|
32
|
+
sortDirection?: SortDirection;
|
|
33
|
+
onSort?: () => void;
|
|
34
|
+
options?: MenuItem[];
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
interface TDProps {
|
|
@@ -83,29 +90,72 @@ function TH({
|
|
|
83
90
|
type = 'standard',
|
|
84
91
|
align = 'left',
|
|
85
92
|
width,
|
|
93
|
+
sortable,
|
|
94
|
+
sortDirection,
|
|
95
|
+
onSort,
|
|
96
|
+
options,
|
|
86
97
|
}: THProps) {
|
|
98
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
99
|
+
|
|
87
100
|
tableStyles.useVariants({
|
|
88
101
|
size,
|
|
89
102
|
type,
|
|
90
103
|
align,
|
|
104
|
+
sortable: !!sortable,
|
|
105
|
+
sortActive: sortDirection != null,
|
|
91
106
|
});
|
|
92
107
|
|
|
93
108
|
const headerCellStyle = (tableStyles.headerCell as any)({});
|
|
109
|
+
const sortIndicatorStyle = (tableStyles.sortIndicator as any)({ sortActive: sortDirection != null });
|
|
110
|
+
const optionsButtonStyle = (tableStyles.optionsButton as any)({});
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
const sortIconName = sortDirection === 'asc' ? 'arrow-up' :
|
|
113
|
+
sortDirection === 'desc' ? 'arrow-down' : 'arrow-up-down';
|
|
114
|
+
|
|
115
|
+
const content = (
|
|
96
116
|
<View
|
|
97
117
|
style={[
|
|
98
118
|
headerCellStyle,
|
|
99
119
|
{ width, flex: width ? undefined : 1 },
|
|
100
120
|
]}
|
|
101
121
|
>
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
122
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, gap: 2 }}>
|
|
123
|
+
{typeof children === 'string' ? (
|
|
124
|
+
<Text style={headerCellStyle}>{children}</Text>
|
|
125
|
+
) : (
|
|
126
|
+
children
|
|
127
|
+
)}
|
|
128
|
+
{sortable && (
|
|
129
|
+
<View style={sortIndicatorStyle}>
|
|
130
|
+
<Icon name={sortIconName} size={size} />
|
|
131
|
+
</View>
|
|
132
|
+
)}
|
|
133
|
+
</View>
|
|
134
|
+
{options && options.length > 0 && (
|
|
135
|
+
<Menu
|
|
136
|
+
items={options}
|
|
137
|
+
open={menuOpen}
|
|
138
|
+
onOpenChange={setMenuOpen}
|
|
139
|
+
placement="bottom-start"
|
|
140
|
+
size={size}
|
|
141
|
+
>
|
|
142
|
+
<Pressable style={optionsButtonStyle}>
|
|
143
|
+
<Icon name="dots-vertical" size={size} />
|
|
144
|
+
</Pressable>
|
|
145
|
+
</Menu>
|
|
106
146
|
)}
|
|
107
147
|
</View>
|
|
108
148
|
);
|
|
149
|
+
|
|
150
|
+
if (sortable) {
|
|
151
|
+
return (
|
|
152
|
+
<Pressable onPress={onSort}>
|
|
153
|
+
{content}
|
|
154
|
+
</Pressable>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return content;
|
|
109
159
|
}
|
|
110
160
|
|
|
111
161
|
// ============================================================================
|
|
@@ -193,6 +243,7 @@ function TableInner<T = any>({
|
|
|
193
243
|
size = 'md',
|
|
194
244
|
stickyHeader: _stickyHeader = false,
|
|
195
245
|
onRowPress,
|
|
246
|
+
onSort,
|
|
196
247
|
dividers = false,
|
|
197
248
|
emptyState,
|
|
198
249
|
// Spacing variants from ContainerStyleProps
|
|
@@ -212,6 +263,26 @@ function TableInner<T = any>({
|
|
|
212
263
|
accessibilityRole,
|
|
213
264
|
accessibilityHidden,
|
|
214
265
|
}: TableProps<T>, ref: React.Ref<ScrollView>) {
|
|
266
|
+
// Sort state
|
|
267
|
+
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
268
|
+
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
269
|
+
|
|
270
|
+
const handleSort = useCallback((columnKey: string) => {
|
|
271
|
+
let newDir: SortDirection;
|
|
272
|
+
if (sortColumn !== columnKey) {
|
|
273
|
+
newDir = 'asc';
|
|
274
|
+
} else if (sortDirection === 'asc') {
|
|
275
|
+
newDir = 'desc';
|
|
276
|
+
} else {
|
|
277
|
+
setSortColumn(null);
|
|
278
|
+
setSortDirection(null);
|
|
279
|
+
onSort?.(columnKey, null);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
setSortColumn(columnKey);
|
|
283
|
+
setSortDirection(newDir);
|
|
284
|
+
onSort?.(columnKey, newDir);
|
|
285
|
+
}, [sortColumn, sortDirection, onSort]);
|
|
215
286
|
// Generate native accessibility props
|
|
216
287
|
const nativeA11yProps = useMemo(() => {
|
|
217
288
|
return getNativeAccessibilityProps({
|
|
@@ -276,7 +347,17 @@ function TableInner<T = any>({
|
|
|
276
347
|
<View style={theadStyle}>
|
|
277
348
|
<View style={{ flexDirection: 'row' }}>
|
|
278
349
|
{cols.map((column) => (
|
|
279
|
-
<TH
|
|
350
|
+
<TH
|
|
351
|
+
key={column.key}
|
|
352
|
+
size={size}
|
|
353
|
+
type={type}
|
|
354
|
+
align={column.align}
|
|
355
|
+
width={column.width}
|
|
356
|
+
sortable={column.sortable}
|
|
357
|
+
sortDirection={sortColumn === column.key ? sortDirection : undefined}
|
|
358
|
+
onSort={column.sortable ? () => handleSort(column.key) : undefined}
|
|
359
|
+
options={column.options}
|
|
360
|
+
>
|
|
280
361
|
{column.title}
|
|
281
362
|
</TH>
|
|
282
363
|
))}
|
|
@@ -23,6 +23,8 @@ export type TableDynamicProps = {
|
|
|
23
23
|
even?: boolean;
|
|
24
24
|
sticky?: boolean;
|
|
25
25
|
align?: CellAlign;
|
|
26
|
+
sortable?: boolean;
|
|
27
|
+
sortActive?: boolean;
|
|
26
28
|
gap?: ViewStyleSize;
|
|
27
29
|
padding?: ViewStyleSize;
|
|
28
30
|
paddingVertical?: ViewStyleSize;
|
|
@@ -178,6 +180,18 @@ export const tableStyles = defineStyle('Table', (theme: Theme) => ({
|
|
|
178
180
|
fontSize: theme.sizes.$table.fontSize,
|
|
179
181
|
lineHeight: theme.sizes.$table.lineHeight,
|
|
180
182
|
},
|
|
183
|
+
sortable: {
|
|
184
|
+
true: {
|
|
185
|
+
_web: {
|
|
186
|
+
cursor: 'pointer',
|
|
187
|
+
userSelect: 'none',
|
|
188
|
+
transition: 'background-color 0.15s ease',
|
|
189
|
+
_hover: {
|
|
190
|
+
backgroundColor: theme.colors.surface.hover,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
181
195
|
},
|
|
182
196
|
_web: {
|
|
183
197
|
position: 'relative',
|
|
@@ -185,6 +199,53 @@ export const tableStyles = defineStyle('Table', (theme: Theme) => ({
|
|
|
185
199
|
},
|
|
186
200
|
}),
|
|
187
201
|
|
|
202
|
+
sortIndicator: ({ sortActive = false }: TableDynamicProps) => ({
|
|
203
|
+
display: 'flex' as const,
|
|
204
|
+
alignItems: 'center' as const,
|
|
205
|
+
justifyContent: 'center' as const,
|
|
206
|
+
marginLeft: 4,
|
|
207
|
+
opacity: sortActive ? 1 : 0.4,
|
|
208
|
+
color: sortActive ? theme.colors.text.primary : theme.colors.text.tertiary,
|
|
209
|
+
flexShrink: 0,
|
|
210
|
+
_web: {
|
|
211
|
+
transition: 'opacity 0.15s ease, color 0.15s ease',
|
|
212
|
+
},
|
|
213
|
+
variants: {
|
|
214
|
+
size: {
|
|
215
|
+
width: theme.sizes.$table.fontSize,
|
|
216
|
+
height: theme.sizes.$table.fontSize,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
|
|
221
|
+
optionsButton: (_props: TableDynamicProps) => ({
|
|
222
|
+
display: 'flex' as const,
|
|
223
|
+
alignItems: 'center' as const,
|
|
224
|
+
justifyContent: 'center' as const,
|
|
225
|
+
marginLeft: 4,
|
|
226
|
+
borderRadius: 4,
|
|
227
|
+
padding: 2,
|
|
228
|
+
opacity: 0.4,
|
|
229
|
+
flexShrink: 0,
|
|
230
|
+
color: theme.colors.text.tertiary,
|
|
231
|
+
_web: {
|
|
232
|
+
cursor: 'pointer',
|
|
233
|
+
border: 'none',
|
|
234
|
+
background: 'transparent',
|
|
235
|
+
transition: 'opacity 0.15s ease, background-color 0.15s ease',
|
|
236
|
+
_hover: {
|
|
237
|
+
opacity: 1,
|
|
238
|
+
backgroundColor: theme.colors.surface.hover,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
variants: {
|
|
242
|
+
size: {
|
|
243
|
+
width: theme.sizes.$table.fontSize,
|
|
244
|
+
height: theme.sizes.$table.fontSize,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
|
|
188
249
|
cell: (_props: TableDynamicProps) => ({
|
|
189
250
|
flexDirection: 'row' as const,
|
|
190
251
|
alignItems: 'center' as const,
|
package/src/Table/Table.web.tsx
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { useMemo, useRef, useCallback, ReactNode } from 'react';
|
|
1
|
+
import { useMemo, useRef, useCallback, useState, ReactNode } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { tableStyles } from './Table.styles';
|
|
4
|
-
import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant } from './types';
|
|
4
|
+
import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant, SortDirection } from './types';
|
|
5
|
+
import type { MenuItem } from '../Menu/types';
|
|
5
6
|
import { getWebAriaProps } from '../utils/accessibility';
|
|
7
|
+
import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
|
|
8
|
+
import Menu from '../Menu/Menu.web';
|
|
6
9
|
|
|
7
10
|
// ============================================================================
|
|
8
11
|
// Helpers
|
|
@@ -49,6 +52,10 @@ interface THProps {
|
|
|
49
52
|
onResize?: (width: number) => void;
|
|
50
53
|
minWidth?: number;
|
|
51
54
|
accessibilitySort?: 'ascending' | 'descending' | 'none' | 'other';
|
|
55
|
+
sortable?: boolean;
|
|
56
|
+
sortDirection?: SortDirection;
|
|
57
|
+
onSort?: () => void;
|
|
58
|
+
options?: MenuItem[];
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
interface TDProps {
|
|
@@ -110,16 +117,37 @@ function TH({
|
|
|
110
117
|
onResize,
|
|
111
118
|
minWidth = 50,
|
|
112
119
|
accessibilitySort,
|
|
120
|
+
sortable,
|
|
121
|
+
sortDirection,
|
|
122
|
+
onSort,
|
|
123
|
+
options,
|
|
113
124
|
}: THProps) {
|
|
125
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
126
|
+
|
|
114
127
|
tableStyles.useVariants({
|
|
115
128
|
size,
|
|
116
129
|
type,
|
|
117
130
|
align,
|
|
131
|
+
sortable: !!sortable,
|
|
132
|
+
sortActive: sortDirection != null,
|
|
118
133
|
});
|
|
119
134
|
|
|
120
135
|
const headerCellProps = getWebProps([(tableStyles.headerCell as any)({})]);
|
|
136
|
+
const sortIndicatorProps = getWebProps([(tableStyles.sortIndicator as any)({ sortActive: sortDirection != null })]);
|
|
137
|
+
const optionsButtonProps = getWebProps([(tableStyles.optionsButton as any)({})]);
|
|
121
138
|
const thRef = useRef<HTMLTableCellElement>(null);
|
|
122
139
|
|
|
140
|
+
// Derive aria-sort from sortDirection
|
|
141
|
+
const derivedAriaSort = accessibilitySort ?? (
|
|
142
|
+
sortDirection === 'asc' ? 'ascending' :
|
|
143
|
+
sortDirection === 'desc' ? 'descending' :
|
|
144
|
+
sortable ? 'none' : undefined
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Sort indicator icon name
|
|
148
|
+
const sortIconName = sortDirection === 'asc' ? 'arrow-up' :
|
|
149
|
+
sortDirection === 'desc' ? 'arrow-down' : 'arrow-up-down';
|
|
150
|
+
|
|
123
151
|
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
|
124
152
|
e.preventDefault();
|
|
125
153
|
e.stopPropagation();
|
|
@@ -139,12 +167,10 @@ function TH({
|
|
|
139
167
|
document.removeEventListener('pointerup', handlePointerUp);
|
|
140
168
|
const finalWidth = th.getBoundingClientRect().width;
|
|
141
169
|
onResize?.(finalWidth);
|
|
142
|
-
// Remove inline cursor override
|
|
143
170
|
document.body.style.cursor = '';
|
|
144
171
|
document.body.style.userSelect = '';
|
|
145
172
|
};
|
|
146
173
|
|
|
147
|
-
// Prevent text selection and set resize cursor globally during drag
|
|
148
174
|
document.body.style.cursor = 'col-resize';
|
|
149
175
|
document.body.style.userSelect = 'none';
|
|
150
176
|
document.addEventListener('pointermove', handlePointerMove);
|
|
@@ -156,10 +182,47 @@ function TH({
|
|
|
156
182
|
{...headerCellProps}
|
|
157
183
|
ref={thRef}
|
|
158
184
|
scope="col"
|
|
159
|
-
aria-sort={
|
|
185
|
+
aria-sort={derivedAriaSort}
|
|
160
186
|
style={{ width, ...getStickyStyle(sticky, stickyOffset, 11) }}
|
|
187
|
+
onClick={sortable ? onSort : undefined}
|
|
161
188
|
>
|
|
162
|
-
{
|
|
189
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
190
|
+
<span style={{ flex: 1, minWidth: 0 }}>
|
|
191
|
+
{children}
|
|
192
|
+
</span>
|
|
193
|
+
{sortable && (
|
|
194
|
+
<span {...sortIndicatorProps} style={{ display: 'inline-flex', flexShrink: 0 }}>
|
|
195
|
+
<IconSvg name={sortIconName} size="0.85em" aria-label={sortIconName} />
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
{options && options.length > 0 && (
|
|
199
|
+
<span onClick={(e) => e.stopPropagation()} style={{ display: 'inline-flex', flexShrink: 0 }}>
|
|
200
|
+
<Menu
|
|
201
|
+
items={options}
|
|
202
|
+
open={menuOpen}
|
|
203
|
+
onOpenChange={setMenuOpen}
|
|
204
|
+
placement="bottom-start"
|
|
205
|
+
size={size}
|
|
206
|
+
>
|
|
207
|
+
<button
|
|
208
|
+
{...optionsButtonProps}
|
|
209
|
+
style={{
|
|
210
|
+
background: 'transparent',
|
|
211
|
+
border: 'none',
|
|
212
|
+
cursor: 'pointer',
|
|
213
|
+
display: 'inline-flex',
|
|
214
|
+
alignItems: 'center',
|
|
215
|
+
justifyContent: 'center',
|
|
216
|
+
padding: 2,
|
|
217
|
+
}}
|
|
218
|
+
aria-label="Column options"
|
|
219
|
+
>
|
|
220
|
+
<IconSvg name="dots-vertical" size="0.85em" aria-label="dots-vertical" />
|
|
221
|
+
</button>
|
|
222
|
+
</Menu>
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
225
|
+
</span>
|
|
163
226
|
{resizable && (
|
|
164
227
|
<span
|
|
165
228
|
onPointerDown={handlePointerDown}
|
|
@@ -266,6 +329,7 @@ function Table<T = any>({
|
|
|
266
329
|
stickyHeader = false,
|
|
267
330
|
onRowPress,
|
|
268
331
|
onColumnResize,
|
|
332
|
+
onSort,
|
|
269
333
|
dividers = false,
|
|
270
334
|
emptyState,
|
|
271
335
|
// Spacing variants from ContainerStyleProps
|
|
@@ -285,6 +349,26 @@ function Table<T = any>({
|
|
|
285
349
|
accessibilityRole,
|
|
286
350
|
accessibilityHidden,
|
|
287
351
|
}: TableProps<T>) {
|
|
352
|
+
// Sort state
|
|
353
|
+
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
354
|
+
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
355
|
+
|
|
356
|
+
const handleSort = useCallback((columnKey: string) => {
|
|
357
|
+
let newDir: SortDirection;
|
|
358
|
+
if (sortColumn !== columnKey) {
|
|
359
|
+
newDir = 'asc';
|
|
360
|
+
} else if (sortDirection === 'asc') {
|
|
361
|
+
newDir = 'desc';
|
|
362
|
+
} else {
|
|
363
|
+
setSortColumn(null);
|
|
364
|
+
setSortDirection(null);
|
|
365
|
+
onSort?.(columnKey, null);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
setSortColumn(columnKey);
|
|
369
|
+
setSortDirection(newDir);
|
|
370
|
+
onSort?.(columnKey, newDir);
|
|
371
|
+
}, [sortColumn, sortDirection, onSort]);
|
|
288
372
|
// Generate ARIA props
|
|
289
373
|
const ariaProps = useMemo(() => {
|
|
290
374
|
return getWebAriaProps({
|
|
@@ -378,6 +462,10 @@ function Table<T = any>({
|
|
|
378
462
|
minWidth={column.minWidth}
|
|
379
463
|
onResize={onColumnResize ? (w) => onColumnResize(column.key, w) : undefined}
|
|
380
464
|
accessibilitySort={column.accessibilitySort}
|
|
465
|
+
sortable={column.sortable}
|
|
466
|
+
sortDirection={sortColumn === column.key ? sortDirection : undefined}
|
|
467
|
+
onSort={column.sortable ? () => handleSort(column.key) : undefined}
|
|
468
|
+
options={column.options}
|
|
381
469
|
>
|
|
382
470
|
{column.title}
|
|
383
471
|
</TH>
|
package/src/Table/types.ts
CHANGED
|
@@ -3,11 +3,13 @@ import type { ReactNode } from 'react';
|
|
|
3
3
|
import { Size } from '@idealyst/theme';
|
|
4
4
|
import { ContainerStyleProps } from '../utils/viewStyleProps';
|
|
5
5
|
import { AccessibilityProps, SortableAccessibilityProps } from '../utils/accessibility';
|
|
6
|
+
import type { MenuItem } from '../Menu/types';
|
|
6
7
|
|
|
7
8
|
// Component-specific type aliases for future extensibility
|
|
8
9
|
export type TableSizeVariant = Size;
|
|
9
10
|
export type TableType = 'standard' | 'striped';
|
|
10
11
|
export type TableAlignVariant = 'left' | 'center' | 'right';
|
|
12
|
+
export type SortDirection = 'asc' | 'desc' | null;
|
|
11
13
|
|
|
12
14
|
export interface TableColumn<T = any> extends SortableAccessibilityProps {
|
|
13
15
|
key: string;
|
|
@@ -32,6 +34,16 @@ export interface TableColumn<T = any> extends SortableAccessibilityProps {
|
|
|
32
34
|
* Minimum width when resizing (default: 50).
|
|
33
35
|
*/
|
|
34
36
|
minWidth?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Enables click-to-sort cycling on this column header.
|
|
39
|
+
* Cycles: unsorted → ascending → descending → unsorted.
|
|
40
|
+
*/
|
|
41
|
+
sortable?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Menu items to show in a column options dropdown.
|
|
44
|
+
* Uses the existing MenuItem type from the Menu component.
|
|
45
|
+
*/
|
|
46
|
+
options?: MenuItem[];
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
/**
|
|
@@ -58,6 +70,11 @@ export interface TableProps<T = any> extends ContainerStyleProps, AccessibilityP
|
|
|
58
70
|
* Receives the column key and the new width in pixels.
|
|
59
71
|
*/
|
|
60
72
|
onColumnResize?: (key: string, width: number) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Called when sort state changes via header click.
|
|
75
|
+
* The Table manages sort state internally; the parent handles data ordering.
|
|
76
|
+
*/
|
|
77
|
+
onSort?: (columnKey: string, direction: SortDirection) => void;
|
|
61
78
|
/**
|
|
62
79
|
* Content to display when `data` is empty.
|
|
63
80
|
* Renders in place of the table body.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { Screen, View, Text, Badge, Button } from '@idealyst/components';
|
|
3
3
|
import Table from '../Table';
|
|
4
|
-
import type { TableColumn } from '../Table/types';
|
|
4
|
+
import type { TableColumn, SortDirection } from '../Table/types';
|
|
5
5
|
|
|
6
6
|
interface User {
|
|
7
7
|
id: number;
|
|
@@ -287,9 +287,102 @@ export const TableExamples: React.FC = () => {
|
|
|
287
287
|
No data to display
|
|
288
288
|
</Text>
|
|
289
289
|
</View>
|
|
290
|
+
|
|
291
|
+
<SortableTableExample />
|
|
292
|
+
<OptionsTableExample />
|
|
290
293
|
</View>
|
|
291
294
|
</Screen>
|
|
292
295
|
);
|
|
293
296
|
};
|
|
294
297
|
|
|
298
|
+
const SortableTableExample: React.FC = () => {
|
|
299
|
+
const [sortedProducts, setSortedProducts] = useState([
|
|
300
|
+
{ id: 1, name: 'Laptop', category: 'Electronics', price: 999.99, stock: 15 },
|
|
301
|
+
{ id: 2, name: 'Mouse', category: 'Electronics', price: 29.99, stock: 150 },
|
|
302
|
+
{ id: 3, name: 'Keyboard', category: 'Electronics', price: 79.99, stock: 75 },
|
|
303
|
+
{ id: 4, name: 'Monitor', category: 'Electronics', price: 299.99, stock: 30 },
|
|
304
|
+
{ id: 5, name: 'Desk', category: 'Furniture', price: 399.99, stock: 10 },
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
const handleSort = (columnKey: string, direction: SortDirection) => {
|
|
308
|
+
if (!direction) {
|
|
309
|
+
setSortedProducts([...sortedProducts]);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const sorted = [...sortedProducts].sort((a, b) => {
|
|
313
|
+
const aVal = (a as any)[columnKey];
|
|
314
|
+
const bVal = (b as any)[columnKey];
|
|
315
|
+
if (typeof aVal === 'number') return direction === 'asc' ? aVal - bVal : bVal - aVal;
|
|
316
|
+
return direction === 'asc'
|
|
317
|
+
? String(aVal).localeCompare(String(bVal))
|
|
318
|
+
: String(bVal).localeCompare(String(aVal));
|
|
319
|
+
});
|
|
320
|
+
setSortedProducts(sorted);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const columns: TableColumn<typeof sortedProducts[0]>[] = [
|
|
324
|
+
{ key: 'name', title: 'Product', dataIndex: 'name', sortable: true },
|
|
325
|
+
{ key: 'category', title: 'Category', dataIndex: 'category', width: '150px', sortable: true },
|
|
326
|
+
{ key: 'price', title: 'Price', dataIndex: 'price', width: '120px', align: 'right', sortable: true, render: (price: number) => `$${price.toFixed(2)}` },
|
|
327
|
+
{ key: 'stock', title: 'Stock', dataIndex: 'stock', width: '100px', align: 'center', sortable: true },
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<View gap="md">
|
|
332
|
+
<Text typography="h5">Sortable Table</Text>
|
|
333
|
+
<Table columns={columns} data={sortedProducts} onSort={handleSort} dividers />
|
|
334
|
+
<Text typography="caption" color="secondary">
|
|
335
|
+
Click column headers to cycle sort: unsorted, ascending, descending
|
|
336
|
+
</Text>
|
|
337
|
+
</View>
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const OptionsTableExample: React.FC = () => {
|
|
342
|
+
const data = [
|
|
343
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
|
|
344
|
+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
|
|
345
|
+
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'User' },
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
const columns: TableColumn<typeof data[0]>[] = [
|
|
349
|
+
{
|
|
350
|
+
key: 'name',
|
|
351
|
+
title: 'Name',
|
|
352
|
+
dataIndex: 'name',
|
|
353
|
+
sortable: true,
|
|
354
|
+
options: [
|
|
355
|
+
{ id: 'sort-asc', label: 'Sort A-Z', icon: 'sort-ascending', onClick: () => console.log('Sort A-Z') },
|
|
356
|
+
{ id: 'sort-desc', label: 'Sort Z-A', icon: 'sort-descending', onClick: () => console.log('Sort Z-A') },
|
|
357
|
+
{ id: 'sep', label: '', separator: true },
|
|
358
|
+
{ id: 'hide', label: 'Hide Column', icon: 'eye-off', onClick: () => console.log('Hide column') },
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
key: 'email',
|
|
363
|
+
title: 'Email',
|
|
364
|
+
dataIndex: 'email',
|
|
365
|
+
options: [
|
|
366
|
+
{ id: 'copy', label: 'Copy All Emails', icon: 'content-copy', onClick: () => console.log('Copy emails') },
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
key: 'role',
|
|
371
|
+
title: 'Role',
|
|
372
|
+
dataIndex: 'role',
|
|
373
|
+
width: '120px',
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<View gap="md">
|
|
379
|
+
<Text typography="h5">Column Options Menu</Text>
|
|
380
|
+
<Table columns={columns} data={data} dividers />
|
|
381
|
+
<Text typography="caption" color="secondary">
|
|
382
|
+
Columns with options show a kebab menu icon in the header
|
|
383
|
+
</Text>
|
|
384
|
+
</View>
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
|
|
295
388
|
export default TableExamples;
|