@idealyst/components 1.2.32 → 1.2.34
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 +3 -3
- package/src/Accordion/Accordion.web.tsx +9 -1
- package/src/Button/Button.web.tsx +1 -3
- package/src/Card/Card.native.tsx +2 -0
- package/src/Card/Card.web.tsx +7 -2
- package/src/Card/types.ts +7 -1
- package/src/Checkbox/Checkbox.web.tsx +2 -0
- package/src/Chip/Chip.web.tsx +4 -1
- package/src/IconButton/IconButton.web.tsx +1 -1
- package/src/List/ListItem.web.tsx +3 -1
- package/src/Menu/MenuItem.web.tsx +5 -1
- package/src/Pressable/Pressable.web.tsx +10 -3
- package/src/RadioButton/RadioButton.web.tsx +3 -1
- package/src/Screen/Screen.native.tsx +3 -1
- package/src/Screen/Screen.web.tsx +5 -1
- package/src/Screen/types.ts +7 -1
- package/src/Select/Select.styles.tsx +36 -32
- package/src/Slider/Slider.web.tsx +7 -0
- package/src/Switch/Switch.web.tsx +3 -1
- package/src/TabBar/TabBar.web.tsx +9 -3
- package/src/TextArea/TextArea.web.tsx +1 -0
- package/src/TextInput/TextInput.web.tsx +1 -0
- package/src/View/View.native.tsx +3 -1
- package/src/View/View.web.tsx +5 -1
- package/src/View/types.ts +8 -1
- package/src/examples/CardExamples.tsx +27 -0
- package/src/examples/ScreenExamples.tsx +31 -0
- package/src/hooks/useWebLayout/index.native.ts +1 -0
- package/src/hooks/useWebLayout/index.ts +1 -0
- package/src/hooks/useWebLayout/index.web.ts +1 -0
- package/src/hooks/useWebLayout/useWebLayout.native.ts +15 -0
- package/src/hooks/useWebLayout/useWebLayout.web.ts +60 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.34",
|
|
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.34",
|
|
60
60
|
"@mdi/js": ">=7.0.0",
|
|
61
61
|
"@mdi/react": ">=1.0.0",
|
|
62
62
|
"@react-native-vector-icons/common": ">=12.0.0",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
|
-
"@idealyst/theme": "^1.2.
|
|
109
|
+
"@idealyst/theme": "^1.2.34",
|
|
110
110
|
"@idealyst/tooling": "^1.2.30",
|
|
111
111
|
"@mdi/react": "^1.6.1",
|
|
112
112
|
"@types/react": "^19.1.0",
|
|
@@ -61,7 +61,11 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
|
|
|
61
61
|
<button
|
|
62
62
|
{...headerProps}
|
|
63
63
|
id={headerId}
|
|
64
|
-
onClick={
|
|
64
|
+
onClick={(e: React.MouseEvent) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
onToggle();
|
|
68
|
+
}}
|
|
65
69
|
onKeyDown={(e) => onKeyDown(e, item.id)}
|
|
66
70
|
disabled={item.disabled}
|
|
67
71
|
aria-expanded={isExpanded}
|
|
@@ -146,16 +150,20 @@ const Accordion: React.FC<AccordionProps> = ({
|
|
|
146
150
|
// ArrowDown moves to next item
|
|
147
151
|
if (key === 'ArrowDown') {
|
|
148
152
|
e.preventDefault();
|
|
153
|
+
e.stopPropagation();
|
|
149
154
|
nextIndex = currentIndex < enabledItems.length - 1 ? currentIndex + 1 : 0;
|
|
150
155
|
// ArrowUp moves to previous item
|
|
151
156
|
} else if (key === 'ArrowUp') {
|
|
152
157
|
e.preventDefault();
|
|
158
|
+
e.stopPropagation();
|
|
153
159
|
nextIndex = currentIndex > 0 ? currentIndex - 1 : enabledItems.length - 1;
|
|
154
160
|
} else if (ACCORDION_KEYS.first.includes(key as 'Home')) {
|
|
155
161
|
e.preventDefault();
|
|
162
|
+
e.stopPropagation();
|
|
156
163
|
nextIndex = 0;
|
|
157
164
|
} else if (ACCORDION_KEYS.last.includes(key as 'End')) {
|
|
158
165
|
e.preventDefault();
|
|
166
|
+
e.stopPropagation();
|
|
159
167
|
nextIndex = enabledItems.length - 1;
|
|
160
168
|
}
|
|
161
169
|
|
|
@@ -64,10 +64,8 @@ const Button = forwardRef<IdealystElement, ButtonProps>((props, ref) => {
|
|
|
64
64
|
|
|
65
65
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
66
66
|
e.preventDefault();
|
|
67
|
-
|
|
68
|
-
// Otherwise, let clicks bubble up to parent handlers (e.g., Menu triggers)
|
|
67
|
+
e.stopPropagation();
|
|
69
68
|
if (!isDisabled && pressHandler) {
|
|
70
|
-
e.stopPropagation();
|
|
71
69
|
pressHandler();
|
|
72
70
|
}
|
|
73
71
|
};
|
package/src/Card/Card.native.tsx
CHANGED
|
@@ -17,6 +17,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
|
|
|
17
17
|
onPress,
|
|
18
18
|
onClick,
|
|
19
19
|
disabled = false,
|
|
20
|
+
onLayout,
|
|
20
21
|
// Spacing variants from ContainerStyleProps
|
|
21
22
|
gap,
|
|
22
23
|
padding,
|
|
@@ -93,6 +94,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
|
|
|
93
94
|
nativeID: id,
|
|
94
95
|
style: [cardStyle, style],
|
|
95
96
|
testID,
|
|
97
|
+
onLayout,
|
|
96
98
|
...nativeA11yProps,
|
|
97
99
|
...(clickable && {
|
|
98
100
|
onPress: disabled ? undefined : pressHandler,
|
package/src/Card/Card.web.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
|
|
|
3
3
|
import { CardProps } from './types';
|
|
4
4
|
import { cardStyles } from './Card.styles';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
|
+
import { useWebLayout } from '../hooks/useWebLayout';
|
|
6
7
|
import { getWebInteractiveAriaProps } from '../utils/accessibility';
|
|
7
8
|
import type { IdealystElement } from '../utils/refTypes';
|
|
8
9
|
|
|
@@ -23,6 +24,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
|
|
|
23
24
|
onPress,
|
|
24
25
|
onClick,
|
|
25
26
|
disabled = false,
|
|
27
|
+
onLayout,
|
|
26
28
|
// Spacing variants from ContainerStyleProps
|
|
27
29
|
gap,
|
|
28
30
|
padding,
|
|
@@ -43,6 +45,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
|
|
|
43
45
|
accessibilityPressed,
|
|
44
46
|
}, ref) => {
|
|
45
47
|
const hasWarnedRef = useRef(false);
|
|
48
|
+
const layoutRef = useWebLayout<HTMLElement>(onLayout);
|
|
46
49
|
|
|
47
50
|
// Warn about onClick usage (deprecated)
|
|
48
51
|
useEffect(() => {
|
|
@@ -72,7 +75,9 @@ const Card = forwardRef<IdealystElement, CardProps>(({
|
|
|
72
75
|
});
|
|
73
76
|
}, [accessibilityLabel, accessibilityHint, accessibilityDisabled, disabled, accessibilityHidden, accessibilityRole, clickable, accessibilityPressed]);
|
|
74
77
|
|
|
75
|
-
const handleClick = () => {
|
|
78
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
76
81
|
if (!disabled && clickable) {
|
|
77
82
|
// Prefer onPress, fall back to deprecated onClick
|
|
78
83
|
const handler = onPress ?? onClick;
|
|
@@ -103,7 +108,7 @@ const Card = forwardRef<IdealystElement, CardProps>(({
|
|
|
103
108
|
// Generate web props
|
|
104
109
|
const webProps = getWebProps([cardStyle, style as any]);
|
|
105
110
|
|
|
106
|
-
const mergedRef = useMergeRefs(ref, webProps.ref);
|
|
111
|
+
const mergedRef = useMergeRefs(ref, webProps.ref, layoutRef);
|
|
107
112
|
|
|
108
113
|
// Use appropriate HTML element based on clickable state
|
|
109
114
|
const Component: any = clickable ? 'button' : 'div';
|
package/src/Card/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Intent, Size } from '@idealyst/theme';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { StyleProp, ViewStyle, LayoutChangeEvent } from 'react-native';
|
|
4
4
|
import { ContainerStyleProps } from '../utils/viewStyleProps';
|
|
5
5
|
import { InteractiveAccessibilityProps } from '../utils/accessibility';
|
|
6
6
|
|
|
@@ -78,4 +78,10 @@ export interface CardProps extends ContainerStyleProps, InteractiveAccessibility
|
|
|
78
78
|
* Test ID for testing
|
|
79
79
|
*/
|
|
80
80
|
testID?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Called when the layout of the card changes.
|
|
84
|
+
* Provides the new width, height, x, and y coordinates.
|
|
85
|
+
*/
|
|
86
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
81
87
|
}
|
|
@@ -53,6 +53,8 @@ const Checkbox = forwardRef<IdealystElement, CheckboxProps>(({
|
|
|
53
53
|
}, [checked]);
|
|
54
54
|
|
|
55
55
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.stopPropagation();
|
|
56
58
|
if (disabled) return;
|
|
57
59
|
|
|
58
60
|
const newChecked = event.target.checked;
|
package/src/Chip/Chip.web.tsx
CHANGED
|
@@ -61,7 +61,9 @@ const Chip = forwardRef<IdealystElement, ChipProps>(({
|
|
|
61
61
|
const deleteButtonProps = getWebProps([(chipStyles.deleteButton as any)({ size })]);
|
|
62
62
|
const deleteIconProps = getWebProps([(chipStyles.deleteIcon as any)({ size, intent, type, selected: isSelected })]);
|
|
63
63
|
|
|
64
|
-
const handleClick = () => {
|
|
64
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
e.stopPropagation();
|
|
65
67
|
if (disabled) return;
|
|
66
68
|
// Prefer onPress, fall back to deprecated onClick
|
|
67
69
|
const handler = onPress ?? onClick;
|
|
@@ -71,6 +73,7 @@ const Chip = forwardRef<IdealystElement, ChipProps>(({
|
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
const handleDelete = (e: React.MouseEvent) => {
|
|
76
|
+
e.preventDefault();
|
|
74
77
|
e.stopPropagation();
|
|
75
78
|
if (disabled) return;
|
|
76
79
|
if (onDelete) {
|
|
@@ -62,8 +62,8 @@ const IconButton = forwardRef<IdealystElement, IconButtonProps>((props, ref) =>
|
|
|
62
62
|
|
|
63
63
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
64
64
|
e.preventDefault();
|
|
65
|
+
e.stopPropagation();
|
|
65
66
|
if (!isDisabled && pressHandler) {
|
|
66
|
-
e.stopPropagation();
|
|
67
67
|
pressHandler();
|
|
68
68
|
}
|
|
69
69
|
};
|
|
@@ -53,7 +53,9 @@ const ListItem: React.FC<ListItemProps & { isLast?: boolean }> = ({
|
|
|
53
53
|
const leadingProps = getWebProps([leadingStyle]);
|
|
54
54
|
const trailingProps = getWebProps([trailingStyle]);
|
|
55
55
|
|
|
56
|
-
const handleClick = () => {
|
|
56
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
e.stopPropagation();
|
|
57
59
|
if (!disabled && onPress) {
|
|
58
60
|
onPress();
|
|
59
61
|
}
|
|
@@ -69,7 +69,11 @@ const MenuItem = forwardRef<IdealystElement, MenuItemProps>(({ item, onPress, si
|
|
|
69
69
|
{...itemProps}
|
|
70
70
|
ref={mergedRef}
|
|
71
71
|
style={buttonResetStyles}
|
|
72
|
-
onClick={() =>
|
|
72
|
+
onClick={(e: React.MouseEvent) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
onPress(item);
|
|
76
|
+
}}
|
|
73
77
|
disabled={item.disabled}
|
|
74
78
|
role="menuitem"
|
|
75
79
|
aria-disabled={item.disabled}
|
|
@@ -23,19 +23,25 @@ const Pressable = forwardRef<IdealystElement, PressableProps>(({
|
|
|
23
23
|
}, ref) => {
|
|
24
24
|
const [_isPressed, setIsPressed] = useState(false);
|
|
25
25
|
|
|
26
|
-
const handleMouseDown = useCallback(() => {
|
|
26
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
e.stopPropagation();
|
|
27
29
|
if (disabled) return;
|
|
28
30
|
setIsPressed(true);
|
|
29
31
|
onPressIn?.();
|
|
30
32
|
}, [disabled, onPressIn]);
|
|
31
33
|
|
|
32
|
-
const handleMouseUp = useCallback(() => {
|
|
34
|
+
const handleMouseUp = useCallback((e: React.MouseEvent) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
e.stopPropagation();
|
|
33
37
|
if (disabled) return;
|
|
34
38
|
setIsPressed(false);
|
|
35
39
|
onPressOut?.();
|
|
36
40
|
}, [disabled, onPressOut]);
|
|
37
41
|
|
|
38
|
-
const handleClick = useCallback(() => {
|
|
42
|
+
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
e.stopPropagation();
|
|
39
45
|
if (disabled) return;
|
|
40
46
|
onPress?.();
|
|
41
47
|
}, [disabled, onPress]);
|
|
@@ -44,6 +50,7 @@ const Pressable = forwardRef<IdealystElement, PressableProps>(({
|
|
|
44
50
|
if (disabled) return;
|
|
45
51
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
46
52
|
event.preventDefault();
|
|
53
|
+
event.stopPropagation();
|
|
47
54
|
onPress?.();
|
|
48
55
|
}
|
|
49
56
|
}, [disabled, onPress]);
|
|
@@ -39,7 +39,9 @@ const RadioButton: React.FC<RadioButtonProps> = ({
|
|
|
39
39
|
const checked = group.value !== undefined ? group.value === value : checkedProp;
|
|
40
40
|
const disabled = group.disabled || disabledProp;
|
|
41
41
|
|
|
42
|
-
const handleClick = () => {
|
|
42
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
e.stopPropagation();
|
|
43
45
|
if (!disabled) {
|
|
44
46
|
if (group.onValueChange) {
|
|
45
47
|
group.onValueChange(value);
|
|
@@ -11,6 +11,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
|
|
|
11
11
|
safeArea = true,
|
|
12
12
|
scrollable = true,
|
|
13
13
|
contentInset,
|
|
14
|
+
onLayout,
|
|
14
15
|
// Spacing variants from ContainerStyleProps
|
|
15
16
|
gap,
|
|
16
17
|
padding,
|
|
@@ -66,6 +67,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
|
|
|
66
67
|
style={[screenStyle, style]}
|
|
67
68
|
contentContainerStyle={{ flexGrow: 1 }}
|
|
68
69
|
testID={testID}
|
|
70
|
+
onLayout={onLayout}
|
|
69
71
|
>
|
|
70
72
|
<RNView style={[contentInsetStyle, { flex: 1 }]}>
|
|
71
73
|
{children}
|
|
@@ -75,7 +77,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
return (
|
|
78
|
-
<RNView ref={ref as any} nativeID={id} style={[screenStyle, safeAreaStyle, style]} testID={testID}>
|
|
80
|
+
<RNView ref={ref as any} nativeID={id} style={[screenStyle, safeAreaStyle, style]} testID={testID} onLayout={onLayout}>
|
|
79
81
|
{children}
|
|
80
82
|
</RNView>
|
|
81
83
|
);
|
|
@@ -3,6 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
|
|
|
3
3
|
import { ScreenProps } from './types';
|
|
4
4
|
import { screenStyles } from './Screen.styles';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
|
+
import { useWebLayout } from '../hooks/useWebLayout';
|
|
6
7
|
import type { IdealystElement } from '../utils/refTypes';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -13,6 +14,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
|
|
|
13
14
|
children,
|
|
14
15
|
background = 'screen',
|
|
15
16
|
safeArea = false,
|
|
17
|
+
onLayout,
|
|
16
18
|
// Spacing variants from ContainerStyleProps
|
|
17
19
|
gap,
|
|
18
20
|
padding,
|
|
@@ -25,6 +27,8 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
|
|
|
25
27
|
testID,
|
|
26
28
|
id,
|
|
27
29
|
}, ref) => {
|
|
30
|
+
const layoutRef = useWebLayout<HTMLDivElement>(onLayout);
|
|
31
|
+
|
|
28
32
|
screenStyles.useVariants({
|
|
29
33
|
background,
|
|
30
34
|
safeArea,
|
|
@@ -40,7 +44,7 @@ const Screen = forwardRef<IdealystElement, ScreenProps>(({
|
|
|
40
44
|
// Call style as function to get theme-reactive styles
|
|
41
45
|
const webProps = getWebProps([(screenStyles.screen as any)({}), style as any]);
|
|
42
46
|
|
|
43
|
-
const mergedRef = useMergeRefs(ref, webProps.ref);
|
|
47
|
+
const mergedRef = useMergeRefs(ref, webProps.ref, layoutRef);
|
|
44
48
|
|
|
45
49
|
return (
|
|
46
50
|
<div
|
package/src/Screen/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { StyleProp, ViewStyle, LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import { Surface } from '@idealyst/theme';
|
|
4
4
|
import { ContainerStyleProps } from '../utils/viewStyleProps';
|
|
5
5
|
|
|
@@ -49,4 +49,10 @@ export interface ScreenProps extends ContainerStyleProps {
|
|
|
49
49
|
* Scrollable content
|
|
50
50
|
*/
|
|
51
51
|
scrollable?: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Called when the layout of the screen changes.
|
|
55
|
+
* Provides the new width, height, x, and y coordinates.
|
|
56
|
+
*/
|
|
57
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
52
58
|
}
|
|
@@ -56,39 +56,43 @@ export const selectStyles = defineStyle('Select', (theme: Theme) => ({
|
|
|
56
56
|
marginBottom: 4,
|
|
57
57
|
}),
|
|
58
58
|
|
|
59
|
-
trigger: ({ type
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
59
|
+
trigger: ({ type = 'outlined', intent = 'neutral', disabled = false, error = false, focused = false }: SelectDynamicProps) => {
|
|
60
|
+
// Determine border color based on state priority: error > focused > default
|
|
61
|
+
const getBorderColor = () => {
|
|
62
|
+
if (error) return theme.intents.danger.primary;
|
|
63
|
+
if (focused) return theme.intents[intent]?.primary ?? theme.intents.primary.primary;
|
|
64
|
+
return type === 'filled' ? 'transparent' : theme.colors.border.primary;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const borderColor = getBorderColor();
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
position: 'relative' as const,
|
|
71
|
+
flexDirection: 'row' as const,
|
|
72
|
+
alignItems: 'center' as const,
|
|
73
|
+
justifyContent: 'space-between' as const,
|
|
74
|
+
borderWidth: 1,
|
|
75
|
+
borderStyle: 'solid' as const,
|
|
76
|
+
borderColor,
|
|
77
|
+
borderRadius: 8,
|
|
78
|
+
opacity: disabled ? 0.6 : 1,
|
|
79
|
+
backgroundColor: type === 'filled' ? theme.colors.surface.secondary : theme.colors.surface.primary,
|
|
80
|
+
variants: {
|
|
81
|
+
size: theme.sizes.$select,
|
|
78
82
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
}
|
|
83
|
+
_web: {
|
|
84
|
+
display: 'flex',
|
|
85
|
+
boxSizing: 'border-box',
|
|
86
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
87
|
+
border: `1px solid ${borderColor}`,
|
|
88
|
+
boxShadow: focused && !error ? `0 0 0 2px ${theme.intents[intent]?.primary ?? theme.intents.primary.primary}33` : 'none',
|
|
89
|
+
transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
|
|
90
|
+
_hover: disabled ? {} : { borderColor: focused || error ? borderColor : theme.colors.border.secondary },
|
|
91
|
+
_active: disabled ? {} : { opacity: 0.9 },
|
|
92
|
+
_focus: { outline: 'none' },
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
92
96
|
|
|
93
97
|
triggerContent: (_props: SelectDynamicProps) => ({
|
|
94
98
|
flex: 1,
|
|
@@ -101,6 +101,7 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
101
101
|
if (disabled) return;
|
|
102
102
|
|
|
103
103
|
e.preventDefault();
|
|
104
|
+
e.stopPropagation();
|
|
104
105
|
|
|
105
106
|
// Check if click is on the thumb
|
|
106
107
|
const isThumbClick = thumbRef.current && thumbRef.current.contains(e.target as Node);
|
|
@@ -145,21 +146,27 @@ const Slider = forwardRef<IdealystElement, SliderProps>(({
|
|
|
145
146
|
|
|
146
147
|
if (matchesKey(e, SLIDER_KEYS.increase)) {
|
|
147
148
|
e.preventDefault();
|
|
149
|
+
e.stopPropagation();
|
|
148
150
|
newValue = clampValue(value + step);
|
|
149
151
|
} else if (matchesKey(e, SLIDER_KEYS.decrease)) {
|
|
150
152
|
e.preventDefault();
|
|
153
|
+
e.stopPropagation();
|
|
151
154
|
newValue = clampValue(value - step);
|
|
152
155
|
} else if (matchesKey(e, SLIDER_KEYS.min)) {
|
|
153
156
|
e.preventDefault();
|
|
157
|
+
e.stopPropagation();
|
|
154
158
|
newValue = min;
|
|
155
159
|
} else if (matchesKey(e, SLIDER_KEYS.max)) {
|
|
156
160
|
e.preventDefault();
|
|
161
|
+
e.stopPropagation();
|
|
157
162
|
newValue = max;
|
|
158
163
|
} else if (matchesKey(e, SLIDER_KEYS.increaseLarge)) {
|
|
159
164
|
e.preventDefault();
|
|
165
|
+
e.stopPropagation();
|
|
160
166
|
newValue = clampValue(value + largeStep);
|
|
161
167
|
} else if (matchesKey(e, SLIDER_KEYS.decreaseLarge)) {
|
|
162
168
|
e.preventDefault();
|
|
169
|
+
e.stopPropagation();
|
|
163
170
|
newValue = clampValue(value - largeStep);
|
|
164
171
|
}
|
|
165
172
|
|
|
@@ -39,7 +39,9 @@ const Switch = forwardRef<IdealystElement, SwitchProps>(({
|
|
|
39
39
|
accessibilityDescribedBy,
|
|
40
40
|
accessibilityChecked,
|
|
41
41
|
}, ref) => {
|
|
42
|
-
const handleClick = () => {
|
|
42
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
e.stopPropagation();
|
|
43
45
|
if (!disabled && onChange) {
|
|
44
46
|
onChange(!checked);
|
|
45
47
|
}
|
|
@@ -36,7 +36,7 @@ function renderIcon(
|
|
|
36
36
|
interface TabProps {
|
|
37
37
|
item: TabBarItem;
|
|
38
38
|
isActive: boolean;
|
|
39
|
-
onClick: () => void;
|
|
39
|
+
onClick: (e: React.MouseEvent) => void;
|
|
40
40
|
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
41
41
|
size: TabBarProps['size'];
|
|
42
42
|
type: TabBarProps['type'];
|
|
@@ -168,15 +168,19 @@ const TabBar: React.FC<TabBarProps> = ({
|
|
|
168
168
|
|
|
169
169
|
if (key === 'ArrowRight') {
|
|
170
170
|
e.preventDefault();
|
|
171
|
+
e.stopPropagation();
|
|
171
172
|
nextIndex = currentIndex < enabledItems.length - 1 ? currentIndex + 1 : 0;
|
|
172
173
|
} else if (key === 'ArrowLeft') {
|
|
173
174
|
e.preventDefault();
|
|
175
|
+
e.stopPropagation();
|
|
174
176
|
nextIndex = currentIndex > 0 ? currentIndex - 1 : enabledItems.length - 1;
|
|
175
177
|
} else if (key === 'Home') {
|
|
176
178
|
e.preventDefault();
|
|
179
|
+
e.stopPropagation();
|
|
177
180
|
nextIndex = 0;
|
|
178
181
|
} else if (key === 'End') {
|
|
179
182
|
e.preventDefault();
|
|
183
|
+
e.stopPropagation();
|
|
180
184
|
nextIndex = enabledItems.length - 1;
|
|
181
185
|
}
|
|
182
186
|
|
|
@@ -237,7 +241,9 @@ const TabBar: React.FC<TabBarProps> = ({
|
|
|
237
241
|
updateIndicator();
|
|
238
242
|
}, [items]);
|
|
239
243
|
|
|
240
|
-
const handleTabClick = (itemValue: string, disabled?: boolean) => {
|
|
244
|
+
const handleTabClick = (e: React.MouseEvent, itemValue: string, disabled?: boolean) => {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
e.stopPropagation();
|
|
241
247
|
if (disabled) return;
|
|
242
248
|
|
|
243
249
|
if (controlledValue === undefined) {
|
|
@@ -306,7 +312,7 @@ const TabBar: React.FC<TabBarProps> = ({
|
|
|
306
312
|
key={item.value}
|
|
307
313
|
item={item}
|
|
308
314
|
isActive={isActive}
|
|
309
|
-
onClick={() => handleTabClick(item.value, item.disabled)}
|
|
315
|
+
onClick={(e) => handleTabClick(e, item.value, item.disabled)}
|
|
310
316
|
onKeyDown={(e) => handleKeyDown(e, item.value)}
|
|
311
317
|
size={size}
|
|
312
318
|
type={type}
|
|
@@ -168,6 +168,7 @@ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
|
|
|
168
168
|
}, [value, autoGrow, minHeight, maxHeight]);
|
|
169
169
|
|
|
170
170
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
171
|
+
e.stopPropagation();
|
|
171
172
|
const newValue = e.target.value;
|
|
172
173
|
|
|
173
174
|
if (maxLength && newValue.length > maxLength) {
|
package/src/View/View.native.tsx
CHANGED
|
@@ -45,6 +45,7 @@ const View = forwardRef<IdealystElement, ViewProps>(({
|
|
|
45
45
|
style,
|
|
46
46
|
testID,
|
|
47
47
|
id,
|
|
48
|
+
onLayout,
|
|
48
49
|
}, ref) => {
|
|
49
50
|
// Set active variants for this render
|
|
50
51
|
viewStyles.useVariants({
|
|
@@ -89,6 +90,7 @@ const View = forwardRef<IdealystElement, ViewProps>(({
|
|
|
89
90
|
contentContainerStyle={[viewStyle, overrideStyles]}
|
|
90
91
|
testID={testID}
|
|
91
92
|
nativeID={id}
|
|
93
|
+
onLayout={onLayout}
|
|
92
94
|
>
|
|
93
95
|
{children}
|
|
94
96
|
</RNScrollView>
|
|
@@ -96,7 +98,7 @@ const View = forwardRef<IdealystElement, ViewProps>(({
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
return (
|
|
99
|
-
<RNView ref={ref as any} style={[viewStyle, overrideStyles, finalStyle]} testID={testID} nativeID={id}>
|
|
101
|
+
<RNView ref={ref as any} style={[viewStyle, overrideStyles, finalStyle]} testID={testID} nativeID={id} onLayout={onLayout}>
|
|
100
102
|
{children}
|
|
101
103
|
</RNView>
|
|
102
104
|
);
|
package/src/View/View.web.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { getWebProps } from 'react-native-unistyles/web';
|
|
|
3
3
|
import { ViewProps } from './types';
|
|
4
4
|
import { viewStyles } from './View.styles';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
|
+
import { useWebLayout } from '../hooks/useWebLayout';
|
|
6
7
|
import type { IdealystElement } from '../utils/refTypes';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -31,7 +32,10 @@ const View = forwardRef<IdealystElement, ViewProps>(({
|
|
|
31
32
|
style,
|
|
32
33
|
testID,
|
|
33
34
|
id,
|
|
35
|
+
onLayout,
|
|
34
36
|
}, ref) => {
|
|
37
|
+
const layoutRef = useWebLayout<HTMLDivElement>(onLayout);
|
|
38
|
+
|
|
35
39
|
viewStyles.useVariants({
|
|
36
40
|
background,
|
|
37
41
|
radius,
|
|
@@ -51,7 +55,7 @@ const View = forwardRef<IdealystElement, ViewProps>(({
|
|
|
51
55
|
/** @ts-ignore */
|
|
52
56
|
const wrapperWebProps = getWebProps(viewStyles.scrollableWrapper);
|
|
53
57
|
|
|
54
|
-
const mergedRef = useMergeRefs(ref, webProps.ref);
|
|
58
|
+
const mergedRef = useMergeRefs(ref, webProps.ref, layoutRef);
|
|
55
59
|
|
|
56
60
|
// When scrollable, render a wrapper + content structure
|
|
57
61
|
// Wrapper: sizing and margin (positioning in parent layout)
|
package/src/View/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Size, Surface, ResponsiveStyle } from '@idealyst/theme';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { StyleProp, ViewStyle, LayoutChangeEvent } from 'react-native';
|
|
4
4
|
import { ContainerStyleProps } from '../utils/viewStyleProps';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -99,4 +99,11 @@ export interface ViewProps extends ContainerStyleProps {
|
|
|
99
99
|
* Test ID for testing
|
|
100
100
|
*/
|
|
101
101
|
testID?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Callback when the view's layout changes.
|
|
105
|
+
* Called with layout information (x, y, width, height) when the component
|
|
106
|
+
* mounts or when its dimensions change.
|
|
107
|
+
*/
|
|
108
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
102
109
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import { Screen, View, Text, Card, Button } from '../index';
|
|
2
3
|
|
|
3
4
|
export const CardExamples = () => {
|
|
@@ -5,6 +6,8 @@ export const CardExamples = () => {
|
|
|
5
6
|
console.log(`Card pressed: ${cardType}`);
|
|
6
7
|
};
|
|
7
8
|
|
|
9
|
+
const [cardDimensions, setCardDimensions] = useState({ width: 0, height: 0 });
|
|
10
|
+
|
|
8
11
|
return (
|
|
9
12
|
<Screen background="primary" padding="lg">
|
|
10
13
|
<View gap="xl">
|
|
@@ -163,6 +166,30 @@ export const CardExamples = () => {
|
|
|
163
166
|
</View>
|
|
164
167
|
</Card>
|
|
165
168
|
</View>
|
|
169
|
+
|
|
170
|
+
{/* onLayout Example */}
|
|
171
|
+
<View gap="md">
|
|
172
|
+
<Text typography="subtitle1">onLayout Callback</Text>
|
|
173
|
+
<Text typography="caption" color="secondary">
|
|
174
|
+
Track card dimensions as they change
|
|
175
|
+
</Text>
|
|
176
|
+
<Card
|
|
177
|
+
type="outlined"
|
|
178
|
+
padding="md"
|
|
179
|
+
onLayout={(event) => {
|
|
180
|
+
const { width, height } = event.nativeEvent.layout;
|
|
181
|
+
setCardDimensions({ width, height });
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
<Text weight="semibold">Responsive Card</Text>
|
|
185
|
+
<Text typography="caption" color="secondary">
|
|
186
|
+
Width: {Math.round(cardDimensions.width)}px
|
|
187
|
+
</Text>
|
|
188
|
+
<Text typography="caption" color="secondary">
|
|
189
|
+
Height: {Math.round(cardDimensions.height)}px
|
|
190
|
+
</Text>
|
|
191
|
+
</Card>
|
|
192
|
+
</View>
|
|
166
193
|
</View>
|
|
167
194
|
</Screen>
|
|
168
195
|
);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import { Screen, View, Text } from '../index';
|
|
2
3
|
|
|
3
4
|
export const ScreenExamples = () => {
|
|
5
|
+
const [screenDimensions, setScreenDimensions] = useState({ width: 0, height: 0 });
|
|
6
|
+
|
|
4
7
|
return (
|
|
5
8
|
<Screen background="primary" padding="lg">
|
|
6
9
|
<View gap="lg">
|
|
@@ -147,6 +150,34 @@ export const ScreenExamples = () => {
|
|
|
147
150
|
</Screen>
|
|
148
151
|
</View>
|
|
149
152
|
</View>
|
|
153
|
+
|
|
154
|
+
{/* onLayout Example */}
|
|
155
|
+
<View gap="md">
|
|
156
|
+
<Text typography="subtitle1">onLayout Callback</Text>
|
|
157
|
+
<Text typography="caption" color="secondary">
|
|
158
|
+
Track screen dimensions as they change
|
|
159
|
+
</Text>
|
|
160
|
+
<View style={{ height: 120, borderWidth: 1, borderColor: '#ccc' }}>
|
|
161
|
+
<Screen
|
|
162
|
+
background="secondary"
|
|
163
|
+
padding="md"
|
|
164
|
+
onLayout={(event) => {
|
|
165
|
+
const { width, height } = event.nativeEvent.layout;
|
|
166
|
+
setScreenDimensions({ width, height });
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<View style={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}>
|
|
170
|
+
<Text weight="semibold">Responsive Screen</Text>
|
|
171
|
+
<Text typography="caption" color="secondary">
|
|
172
|
+
Width: {Math.round(screenDimensions.width)}px
|
|
173
|
+
</Text>
|
|
174
|
+
<Text typography="caption" color="secondary">
|
|
175
|
+
Height: {Math.round(screenDimensions.height)}px
|
|
176
|
+
</Text>
|
|
177
|
+
</View>
|
|
178
|
+
</Screen>
|
|
179
|
+
</View>
|
|
180
|
+
</View>
|
|
150
181
|
</View>
|
|
151
182
|
</Screen>
|
|
152
183
|
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useWebLayout } from './useWebLayout.native';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useWebLayout } from './useWebLayout';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useWebLayout } from './useWebLayout.web';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import type { LayoutChangeEvent } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* No-op hook for native - onLayout is handled natively by React Native components.
|
|
6
|
+
* Returns a ref for API compatibility, but it's not used on native.
|
|
7
|
+
*
|
|
8
|
+
* @param _onLayout - Unused on native (React Native components handle onLayout directly)
|
|
9
|
+
* @returns A ref (unused on native, for API compatibility)
|
|
10
|
+
*/
|
|
11
|
+
export function useWebLayout<T = any>(
|
|
12
|
+
_onLayout: ((event: LayoutChangeEvent) => void) | undefined
|
|
13
|
+
) {
|
|
14
|
+
return useRef<T>(null);
|
|
15
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import type { LayoutChangeEvent } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook that provides onLayout functionality for web components using ResizeObserver.
|
|
6
|
+
* Returns a ref that should be attached to the element you want to observe.
|
|
7
|
+
*
|
|
8
|
+
* @param onLayout - Callback fired when layout changes, with React Native compatible event shape
|
|
9
|
+
* @returns A ref to attach to the observed element
|
|
10
|
+
*/
|
|
11
|
+
export function useWebLayout<T extends HTMLElement = HTMLElement>(
|
|
12
|
+
onLayout: ((event: LayoutChangeEvent) => void) | undefined
|
|
13
|
+
) {
|
|
14
|
+
const ref = useRef<T>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!onLayout || !ref.current) return;
|
|
18
|
+
|
|
19
|
+
const element = ref.current;
|
|
20
|
+
|
|
21
|
+
// Call immediately with initial layout
|
|
22
|
+
const rect = element.getBoundingClientRect();
|
|
23
|
+
onLayout({
|
|
24
|
+
nativeEvent: {
|
|
25
|
+
layout: {
|
|
26
|
+
x: rect.left,
|
|
27
|
+
y: rect.top,
|
|
28
|
+
width: rect.width,
|
|
29
|
+
height: rect.height,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} as LayoutChangeEvent);
|
|
33
|
+
|
|
34
|
+
// Set up ResizeObserver for subsequent changes
|
|
35
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const { width, height } = entry.contentRect;
|
|
38
|
+
const boundingRect = element.getBoundingClientRect();
|
|
39
|
+
onLayout({
|
|
40
|
+
nativeEvent: {
|
|
41
|
+
layout: {
|
|
42
|
+
x: boundingRect.left,
|
|
43
|
+
y: boundingRect.top,
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
} as LayoutChangeEvent);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
resizeObserver.observe(element);
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
resizeObserver.disconnect();
|
|
56
|
+
};
|
|
57
|
+
}, [onLayout]);
|
|
58
|
+
|
|
59
|
+
return ref;
|
|
60
|
+
}
|