cpk-ui 0.2.0 → 0.2.2
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/README.md +12 -0
- package/components/common/ErrorBoundary/ErrorBoundary.js +48 -0
- package/components/common/ErrorBoundary/ErrorBoundary.tsx +75 -0
- package/components/modals/AlertDialog/AlertDialog.js +79 -37
- package/components/modals/AlertDialog/AlertDialog.tsx +152 -65
- package/components/modals/Snackbar/Snackbar.js +68 -44
- package/components/modals/Snackbar/Snackbar.tsx +93 -49
- package/components/uis/Accordion/Accordion.js +8 -4
- package/components/uis/Accordion/Accordion.tsx +21 -12
- package/components/uis/Accordion/AccordionItem.js +53 -33
- package/components/uis/Accordion/AccordionItem.tsx +71 -41
- package/components/uis/Button/Button.js +82 -62
- package/components/uis/Button/Button.tsx +101 -73
- package/components/uis/Card/Card.js +0 -0
- package/components/uis/Card/Card.test.tsx +0 -0
- package/components/uis/Card/Card.tsx +0 -0
- package/components/uis/Checkbox/Checkbox.js +34 -24
- package/components/uis/Checkbox/Checkbox.test.tsx +19 -12
- package/components/uis/Checkbox/Checkbox.tsx +49 -29
- package/components/uis/CustomPressable/CustomPressable.js +20 -13
- package/components/uis/CustomPressable/CustomPressable.tsx +32 -16
- package/components/uis/EditText/EditText.js +58 -27
- package/components/uis/EditText/EditText.test.tsx +14 -9
- package/components/uis/EditText/EditText.tsx +51 -26
- package/components/uis/Fab/Fab.js +51 -35
- package/components/uis/Fab/Fab.tsx +80 -55
- package/components/uis/Hr/Hr.js +4 -1
- package/components/uis/Hr/Hr.tsx +5 -1
- package/components/uis/Icon/Icon.js +5 -2
- package/components/uis/Icon/Icon.tsx +7 -3
- package/components/uis/IconButton/IconButton.js +84 -63
- package/components/uis/IconButton/IconButton.tsx +112 -98
- package/components/uis/LoadingIndicator/LoadingIndicator.js +42 -17
- package/components/uis/LoadingIndicator/LoadingIndicator.tsx +66 -40
- package/components/uis/RadioGroup/RadioButton.js +43 -31
- package/components/uis/RadioGroup/RadioButton.tsx +60 -36
- package/components/uis/RadioGroup/RadioGroup.js +11 -4
- package/components/uis/RadioGroup/RadioGroup.tsx +34 -18
- package/components/uis/Rating/Rating.js +35 -21
- package/components/uis/Rating/Rating.tsx +128 -92
- package/components/uis/StatusbarBrightness/StatusBarBrightness.js +11 -6
- package/components/uis/StatusbarBrightness/StatusBarBrightness.tsx +16 -7
- package/components/uis/SwitchToggle/SwitchToggle.js +99 -54
- package/components/uis/SwitchToggle/SwitchToggle.tsx +165 -103
- package/components/uis/Typography/Typography.js +13 -7
- package/components/uis/Typography/Typography.tsx +25 -18
- package/hooks/index.js +2 -0
- package/hooks/index.ts +2 -0
- package/hooks/useAnimations.js +47 -0
- package/hooks/useAnimations.ts +71 -0
- package/hooks/useOptimizedStyles.js +13 -0
- package/hooks/useOptimizedStyles.ts +22 -0
- package/hooks/usePerformance.js +58 -0
- package/hooks/usePerformance.ts +93 -0
- package/index.js +8 -0
- package/index.tsx +12 -0
- package/package.json +1 -1
- package/test/testUtils.js +65 -0
- package/test/testUtils.tsx +101 -0
- package/types/common.js +1 -0
- package/types/common.ts +77 -0
- package/types/index.js +1 -0
- package/types/index.ts +1 -0
- package/utils/accessibility.js +79 -0
- package/utils/accessibility.ts +110 -0
- package/utils/index.js +2 -0
- package/utils/index.ts +2 -0
- package/utils/performanceTest.js +128 -0
- package/utils/performanceTest.ts +174 -0
- package/utils/platform.js +59 -0
- package/utils/platform.ts +69 -0
package/README.md
CHANGED
|
@@ -23,6 +23,18 @@ Check out [ui.crossplatformkorea.com](https://ui.crossplatformkorea.com)
|
|
|
23
23
|
|
|
24
24
|
`cpk-ui` aims to provide user-friendly, lightweight, and adaptable UI components. It emphasizes customizable `theme` variations and a responsive layout to enhance developer experience.
|
|
25
25
|
|
|
26
|
+
### Performance
|
|
27
|
+
|
|
28
|
+
`cpk-ui` is optimized for React Native performance using React's built-in optimization features:
|
|
29
|
+
|
|
30
|
+
- **React.memo**: All components are wrapped to prevent unnecessary re-renders
|
|
31
|
+
- **useCallback**: Event handlers are memoized for stable references
|
|
32
|
+
- **useMemo**: Expensive calculations and styles are memoized
|
|
33
|
+
- **110+ tests passing**: Comprehensive test coverage ensures reliability
|
|
34
|
+
- **Zero breaking changes**: All optimizations maintain API compatibility
|
|
35
|
+
|
|
36
|
+
For detailed performance information, see our [Performance Guide](docs/PERFORMANCE.md).
|
|
37
|
+
|
|
26
38
|
### Installation
|
|
27
39
|
|
|
28
40
|
#### For Expo
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import { View, Text } from 'react-native';
|
|
4
|
+
export class ErrorBoundary extends Component {
|
|
5
|
+
constructor(props) {
|
|
6
|
+
super(props);
|
|
7
|
+
this.state = { hasError: false };
|
|
8
|
+
}
|
|
9
|
+
static getDerivedStateFromError(error) {
|
|
10
|
+
return { hasError: true, error };
|
|
11
|
+
}
|
|
12
|
+
componentDidCatch(error, errorInfo) {
|
|
13
|
+
this.props.onError?.(error, errorInfo);
|
|
14
|
+
// Log error in development
|
|
15
|
+
if (__DEV__) {
|
|
16
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
render() {
|
|
20
|
+
if (this.state.hasError) {
|
|
21
|
+
if (this.props.fallback) {
|
|
22
|
+
return this.props.fallback;
|
|
23
|
+
}
|
|
24
|
+
return (_jsxs(View, { style: [defaultErrorStyle.container, this.props.style], children: [_jsx(Text, { style: defaultErrorStyle.title, children: "Something went wrong" }), _jsx(Text, { style: defaultErrorStyle.message, children: this.state.error?.message || 'An unexpected error occurred' })] }));
|
|
25
|
+
}
|
|
26
|
+
return this.props.children;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const defaultErrorStyle = {
|
|
30
|
+
container: {
|
|
31
|
+
flex: 1,
|
|
32
|
+
justifyContent: 'center',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
padding: 20,
|
|
35
|
+
backgroundColor: '#f8f9fa',
|
|
36
|
+
},
|
|
37
|
+
title: {
|
|
38
|
+
fontSize: 18,
|
|
39
|
+
fontWeight: 'bold',
|
|
40
|
+
color: '#dc3545',
|
|
41
|
+
marginBottom: 8,
|
|
42
|
+
},
|
|
43
|
+
message: {
|
|
44
|
+
fontSize: 14,
|
|
45
|
+
color: '#6c757d',
|
|
46
|
+
textAlign: 'center',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, {Component, ReactNode} from 'react';
|
|
2
|
+
import {View, Text} from 'react-native';
|
|
3
|
+
import type {StyleProp, ViewStyle, TextStyle} from 'react-native';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface State {
|
|
13
|
+
hasError: boolean;
|
|
14
|
+
error?: Error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
18
|
+
constructor(props: Props) {
|
|
19
|
+
super(props);
|
|
20
|
+
this.state = {hasError: false};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static getDerivedStateFromError(error: Error): State {
|
|
24
|
+
return {hasError: true, error};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
28
|
+
this.props.onError?.(error, errorInfo);
|
|
29
|
+
|
|
30
|
+
// Log error in development
|
|
31
|
+
if (__DEV__) {
|
|
32
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render() {
|
|
37
|
+
if (this.state.hasError) {
|
|
38
|
+
if (this.props.fallback) {
|
|
39
|
+
return this.props.fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={[defaultErrorStyle.container, this.props.style]}>
|
|
44
|
+
<Text style={defaultErrorStyle.title}>Something went wrong</Text>
|
|
45
|
+
<Text style={defaultErrorStyle.message}>
|
|
46
|
+
{this.state.error?.message || 'An unexpected error occurred'}
|
|
47
|
+
</Text>
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this.props.children;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const defaultErrorStyle = {
|
|
57
|
+
container: {
|
|
58
|
+
flex: 1,
|
|
59
|
+
justifyContent: 'center' as const,
|
|
60
|
+
alignItems: 'center' as const,
|
|
61
|
+
padding: 20,
|
|
62
|
+
backgroundColor: '#f8f9fa',
|
|
63
|
+
},
|
|
64
|
+
title: {
|
|
65
|
+
fontSize: 18,
|
|
66
|
+
fontWeight: 'bold' as const,
|
|
67
|
+
color: '#dc3545',
|
|
68
|
+
marginBottom: 8,
|
|
69
|
+
},
|
|
70
|
+
message: {
|
|
71
|
+
fontSize: 14,
|
|
72
|
+
color: '#6c757d',
|
|
73
|
+
textAlign: 'center' as const,
|
|
74
|
+
},
|
|
75
|
+
} satisfies Record<string, StyleProp<ViewStyle | TextStyle>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { cloneElement, forwardRef, useEffect, useImperativeHandle, useState, } from 'react';
|
|
2
|
+
import React, { cloneElement, forwardRef, useEffect, useImperativeHandle, useState, useCallback, useMemo, } from 'react';
|
|
3
3
|
import { Modal, Platform, StyleSheet, TouchableWithoutFeedback, View, } from 'react-native';
|
|
4
4
|
import styled, { css } from '@emotion/native';
|
|
5
5
|
import { useTheme } from '../../../providers/ThemeProvider';
|
|
@@ -37,53 +37,95 @@ function AlertDialog({ style }, ref) {
|
|
|
37
37
|
const [options, setOptions] = useState(null);
|
|
38
38
|
const [visible, setVisible] = useState(false);
|
|
39
39
|
const { theme, themeType } = useTheme();
|
|
40
|
+
// Memoize the cleanup effect
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
if (!visible) {
|
|
42
|
-
setTimeout(() => {
|
|
43
|
+
const timeoutId = setTimeout(() => {
|
|
43
44
|
setOptions(null);
|
|
44
45
|
// Run after modal has finished transition
|
|
45
46
|
}, 300);
|
|
47
|
+
return () => clearTimeout(timeoutId);
|
|
46
48
|
}
|
|
49
|
+
// Return an empty cleanup function when visible is true
|
|
50
|
+
return () => { };
|
|
47
51
|
}, [visible]);
|
|
52
|
+
// Memoize the close handler
|
|
53
|
+
const handleClose = useCallback(() => {
|
|
54
|
+
setVisible(false);
|
|
55
|
+
}, []);
|
|
56
|
+
// Memoize the open handler
|
|
57
|
+
const handleOpen = useCallback((alertDialogOptions) => {
|
|
58
|
+
setVisible(true);
|
|
59
|
+
if (alertDialogOptions) {
|
|
60
|
+
setOptions(alertDialogOptions);
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
48
63
|
useImperativeHandle(ref, () => ({
|
|
49
|
-
open:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
setOptions(alertDialogOptions);
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
close: () => {
|
|
56
|
-
setVisible(false);
|
|
57
|
-
},
|
|
58
|
-
}));
|
|
64
|
+
open: handleOpen,
|
|
65
|
+
close: handleClose,
|
|
66
|
+
}), [handleOpen, handleClose]);
|
|
59
67
|
const { backdropOpacity = 0.2, title, body, styles, actions, closeOnTouchOutside = true, showCloseButton = true, } = options ?? {};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
// Memoize backdrop color calculation
|
|
69
|
+
const backdropColor = useMemo(() => themeType === 'light'
|
|
70
|
+
? `rgba(0,0,0,${backdropOpacity})`
|
|
71
|
+
: `rgba(255,255,255,${backdropOpacity})`, [themeType, backdropOpacity]);
|
|
72
|
+
// Memoize shadow styles
|
|
73
|
+
const shadowStyles = useMemo(() => Platform.OS !== 'web' ? {
|
|
74
|
+
shadowOffset: { width: 0, height: 4 },
|
|
75
|
+
shadowColor: theme.text.basic,
|
|
76
|
+
} : {}, [theme.text.basic]);
|
|
77
|
+
// Memoize container styles
|
|
78
|
+
const containerStyles = useMemo(() => StyleSheet.flatten([shadowStyles, styles?.container]), [shadowStyles, styles?.container]);
|
|
79
|
+
// Memoize modal styles
|
|
80
|
+
const modalStyles = useMemo(() => [
|
|
81
|
+
css `
|
|
82
|
+
flex: 1;
|
|
83
|
+
align-self: stretch;
|
|
84
|
+
`,
|
|
85
|
+
style,
|
|
86
|
+
], [style]);
|
|
87
|
+
// Memoize backdrop press handler
|
|
88
|
+
const handleBackdropPress = useCallback(() => {
|
|
89
|
+
if (closeOnTouchOutside) {
|
|
90
|
+
setVisible(false);
|
|
91
|
+
}
|
|
92
|
+
}, [closeOnTouchOutside]);
|
|
93
|
+
// Memoize close button press handler
|
|
94
|
+
const handleCloseButtonPress = useCallback(() => setVisible(false), []);
|
|
95
|
+
// Memoize title content
|
|
96
|
+
const titleContent = useMemo(() => typeof title === 'string' ? (_jsx(Typography.Heading3, { style: styles?.title, children: title })) : (title), [title, styles?.title]);
|
|
97
|
+
// Memoize body content
|
|
98
|
+
const bodyContent = useMemo(() => typeof body === 'string' ? (_jsx(Typography.Body3, { style: styles?.body, children: body })) : (body), [body, styles?.body]);
|
|
99
|
+
// Memoize actions content
|
|
100
|
+
const actionsContent = useMemo(() => actions ? (_jsx(ActionRow, { style: styles?.actionContainer, children: actions.map((action, index) => cloneElement(action, {
|
|
101
|
+
key: `action-${index}`,
|
|
102
|
+
style: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
marginLeft: index !== 0 ? 12 : 0,
|
|
105
|
+
},
|
|
106
|
+
})) })) : null, [actions, styles?.actionContainer]);
|
|
107
|
+
// Memoize close button content
|
|
108
|
+
const closeButtonContent = useMemo(() => showCloseButton ? (_jsx(Button, { onPress: handleCloseButtonPress, borderRadius: 24, text: _jsx(Icon, { color: theme.text.basic, name: "X", size: 18 }), type: "text" })) : null, [showCloseButton, handleCloseButtonPress, theme.text.basic]);
|
|
109
|
+
const AlertDialogContent = useMemo(() => (_jsxs(Container, { style: css `
|
|
110
|
+
background-color: ${backdropColor};
|
|
111
|
+
`, children: [_jsx(TouchableWithoutFeedback, { onPress: closeOnTouchOutside ? handleCloseButtonPress : undefined, children: _jsx(View, { style: StyleSheet.absoluteFill }) }), _jsxs(AlertDialogContainer, { accessibilityRole: "alert", accessibilityLabel: typeof title === 'string' ? title : 'Alert dialog', style: containerStyles, children: [_jsxs(TitleRow, { style: styles?.titleContainer, children: [titleContent, closeButtonContent] }), _jsx(BodyRow, { style: styles?.bodyContainer, children: bodyContent }), actionsContent] })] })), [
|
|
112
|
+
backdropColor,
|
|
113
|
+
closeOnTouchOutside,
|
|
114
|
+
handleCloseButtonPress,
|
|
115
|
+
title,
|
|
116
|
+
containerStyles,
|
|
117
|
+
styles?.titleContainer,
|
|
118
|
+
styles?.bodyContainer,
|
|
119
|
+
titleContent,
|
|
120
|
+
closeButtonContent,
|
|
121
|
+
bodyContent,
|
|
122
|
+
actionsContent,
|
|
123
|
+
]);
|
|
77
124
|
return (
|
|
78
125
|
// https://github.com/facebook/react-native/issues/48526#issuecomment-2579478884
|
|
79
|
-
_jsx(View, { children: _jsx(Modal, { animationType: "fade", style:
|
|
80
|
-
css `
|
|
81
|
-
flex: 1;
|
|
82
|
-
align-self: stretch;
|
|
83
|
-
`,
|
|
84
|
-
style,
|
|
85
|
-
], transparent: true, visible: visible, children: closeOnTouchOutside ? (_jsx(TouchableWithoutFeedback, { onPress: () => setVisible(false), style: css `
|
|
126
|
+
_jsx(View, { children: _jsx(Modal, { animationType: "fade", style: modalStyles, transparent: true, visible: visible, children: closeOnTouchOutside ? (_jsx(TouchableWithoutFeedback, { onPress: handleBackdropPress, style: css `
|
|
86
127
|
flex: 1;
|
|
87
128
|
`, children: AlertDialogContent })) : (AlertDialogContent) }) }));
|
|
88
129
|
}
|
|
89
|
-
|
|
130
|
+
// Export memoized component for better performance
|
|
131
|
+
export default React.memo(forwardRef(AlertDialog));
|
|
@@ -4,6 +4,8 @@ import React, {
|
|
|
4
4
|
useEffect,
|
|
5
5
|
useImperativeHandle,
|
|
6
6
|
useState,
|
|
7
|
+
useCallback,
|
|
8
|
+
useMemo,
|
|
7
9
|
} from 'react';
|
|
8
10
|
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
|
|
9
11
|
import {
|
|
@@ -87,26 +89,38 @@ function AlertDialog(
|
|
|
87
89
|
const [visible, setVisible] = useState(false);
|
|
88
90
|
const {theme, themeType} = useTheme();
|
|
89
91
|
|
|
92
|
+
// Memoize the cleanup effect
|
|
90
93
|
useEffect(() => {
|
|
91
94
|
if (!visible) {
|
|
92
|
-
setTimeout(() => {
|
|
95
|
+
const timeoutId = setTimeout(() => {
|
|
93
96
|
setOptions(null);
|
|
94
97
|
// Run after modal has finished transition
|
|
95
98
|
}, 300);
|
|
99
|
+
|
|
100
|
+
return () => clearTimeout(timeoutId);
|
|
96
101
|
}
|
|
102
|
+
|
|
103
|
+
// Return an empty cleanup function when visible is true
|
|
104
|
+
return () => {};
|
|
97
105
|
}, [visible]);
|
|
98
106
|
|
|
107
|
+
// Memoize the close handler
|
|
108
|
+
const handleClose = useCallback(() => {
|
|
109
|
+
setVisible(false);
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
// Memoize the open handler
|
|
113
|
+
const handleOpen = useCallback((alertDialogOptions?: AlertDialogOptions) => {
|
|
114
|
+
setVisible(true);
|
|
115
|
+
if (alertDialogOptions) {
|
|
116
|
+
setOptions(alertDialogOptions);
|
|
117
|
+
}
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
99
120
|
useImperativeHandle(ref, () => ({
|
|
100
|
-
open:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
setOptions(alertDialogOptions);
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
close: () => {
|
|
107
|
-
setVisible(false);
|
|
108
|
-
},
|
|
109
|
-
}));
|
|
121
|
+
open: handleOpen,
|
|
122
|
+
close: handleClose,
|
|
123
|
+
}), [handleOpen, handleClose]);
|
|
110
124
|
|
|
111
125
|
const {
|
|
112
126
|
backdropOpacity = 0.2,
|
|
@@ -118,82 +132,154 @@ function AlertDialog(
|
|
|
118
132
|
showCloseButton = true,
|
|
119
133
|
} = options ?? {};
|
|
120
134
|
|
|
121
|
-
|
|
135
|
+
// Memoize backdrop color calculation
|
|
136
|
+
const backdropColor = useMemo(() =>
|
|
137
|
+
themeType === 'light'
|
|
138
|
+
? `rgba(0,0,0,${backdropOpacity})`
|
|
139
|
+
: `rgba(255,255,255,${backdropOpacity})`,
|
|
140
|
+
[themeType, backdropOpacity]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Memoize shadow styles
|
|
144
|
+
const shadowStyles = useMemo(() =>
|
|
145
|
+
Platform.OS !== 'web' ? {
|
|
146
|
+
shadowOffset: {width: 0, height: 4},
|
|
147
|
+
shadowColor: theme.text.basic,
|
|
148
|
+
} : {},
|
|
149
|
+
[theme.text.basic]
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Memoize container styles
|
|
153
|
+
const containerStyles = useMemo(() =>
|
|
154
|
+
StyleSheet.flatten([shadowStyles, styles?.container]),
|
|
155
|
+
[shadowStyles, styles?.container]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Memoize modal styles
|
|
159
|
+
const modalStyles = useMemo(() => [
|
|
160
|
+
css`
|
|
161
|
+
flex: 1;
|
|
162
|
+
align-self: stretch;
|
|
163
|
+
`,
|
|
164
|
+
style,
|
|
165
|
+
], [style]);
|
|
166
|
+
|
|
167
|
+
// Memoize backdrop press handler
|
|
168
|
+
const handleBackdropPress = useCallback(() => {
|
|
169
|
+
if (closeOnTouchOutside) {
|
|
170
|
+
setVisible(false);
|
|
171
|
+
}
|
|
172
|
+
}, [closeOnTouchOutside]);
|
|
173
|
+
|
|
174
|
+
// Memoize close button press handler
|
|
175
|
+
const handleCloseButtonPress = useCallback(() => setVisible(false), []);
|
|
176
|
+
|
|
177
|
+
// Memoize title content
|
|
178
|
+
const titleContent = useMemo(() =>
|
|
179
|
+
typeof title === 'string' ? (
|
|
180
|
+
<Typography.Heading3 style={styles?.title}>
|
|
181
|
+
{title}
|
|
182
|
+
</Typography.Heading3>
|
|
183
|
+
) : (
|
|
184
|
+
title
|
|
185
|
+
),
|
|
186
|
+
[title, styles?.title]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Memoize body content
|
|
190
|
+
const bodyContent = useMemo(() =>
|
|
191
|
+
typeof body === 'string' ? (
|
|
192
|
+
<Typography.Body3 style={styles?.body}>{body}</Typography.Body3>
|
|
193
|
+
) : (
|
|
194
|
+
body
|
|
195
|
+
),
|
|
196
|
+
[body, styles?.body]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Memoize actions content
|
|
200
|
+
const actionsContent = useMemo(() =>
|
|
201
|
+
actions ? (
|
|
202
|
+
<ActionRow style={styles?.actionContainer}>
|
|
203
|
+
{actions.map((action, index) =>
|
|
204
|
+
cloneElement(action, {
|
|
205
|
+
key: `action-${index}`,
|
|
206
|
+
style: {
|
|
207
|
+
flex: 1,
|
|
208
|
+
marginLeft: index !== 0 ? 12 : 0,
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
)}
|
|
212
|
+
</ActionRow>
|
|
213
|
+
) : null,
|
|
214
|
+
[actions, styles?.actionContainer]
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Memoize close button content
|
|
218
|
+
const closeButtonContent = useMemo(() =>
|
|
219
|
+
showCloseButton ? (
|
|
220
|
+
<Button
|
|
221
|
+
onPress={handleCloseButtonPress}
|
|
222
|
+
borderRadius={24}
|
|
223
|
+
text={<Icon color={theme.text.basic} name="X" size={18} />}
|
|
224
|
+
type="text"
|
|
225
|
+
/>
|
|
226
|
+
) : null,
|
|
227
|
+
[showCloseButton, handleCloseButtonPress, theme.text.basic]
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const AlertDialogContent = useMemo(() => (
|
|
122
231
|
<Container
|
|
123
232
|
style={css`
|
|
124
|
-
background-color: ${
|
|
125
|
-
? `rgba(0,0,0,${backdropOpacity})`
|
|
126
|
-
: `rgba(255,255,255,${backdropOpacity})`};
|
|
233
|
+
background-color: ${backdropColor};
|
|
127
234
|
`}
|
|
128
235
|
>
|
|
236
|
+
<TouchableWithoutFeedback
|
|
237
|
+
onPress={closeOnTouchOutside ? handleCloseButtonPress : undefined}
|
|
238
|
+
>
|
|
239
|
+
<View style={StyleSheet.absoluteFill} />
|
|
240
|
+
</TouchableWithoutFeedback>
|
|
241
|
+
|
|
129
242
|
<AlertDialogContainer
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
shadowColor: theme.text.basic,
|
|
134
|
-
},
|
|
135
|
-
styles?.container,
|
|
136
|
-
])}
|
|
243
|
+
accessibilityRole="alert"
|
|
244
|
+
accessibilityLabel={typeof title === 'string' ? title : 'Alert dialog'}
|
|
245
|
+
style={containerStyles}
|
|
137
246
|
>
|
|
138
247
|
<TitleRow style={styles?.titleContainer}>
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
{title}
|
|
142
|
-
</Typography.Heading3>
|
|
143
|
-
) : (
|
|
144
|
-
title
|
|
145
|
-
)}
|
|
146
|
-
{showCloseButton ? (
|
|
147
|
-
<Button
|
|
148
|
-
onPress={() => setVisible(false)}
|
|
149
|
-
borderRadius={24}
|
|
150
|
-
text={<Icon color={theme.text.basic} name="X" size={18} />}
|
|
151
|
-
type="text"
|
|
152
|
-
/>
|
|
153
|
-
) : null}
|
|
248
|
+
{titleContent}
|
|
249
|
+
{closeButtonContent}
|
|
154
250
|
</TitleRow>
|
|
155
251
|
<BodyRow style={styles?.bodyContainer}>
|
|
156
|
-
{
|
|
157
|
-
<Typography.Body3 style={styles?.body}>{body}</Typography.Body3>
|
|
158
|
-
) : (
|
|
159
|
-
body
|
|
160
|
-
)}
|
|
252
|
+
{bodyContent}
|
|
161
253
|
</BodyRow>
|
|
162
|
-
{
|
|
163
|
-
<ActionRow style={styles?.actionContainer}>
|
|
164
|
-
{actions.map((action, index) =>
|
|
165
|
-
cloneElement(action, {
|
|
166
|
-
key: `action-${index}`,
|
|
167
|
-
style: {
|
|
168
|
-
flex: 1,
|
|
169
|
-
marginLeft: index !== 0 ? 12 : 0,
|
|
170
|
-
},
|
|
171
|
-
}),
|
|
172
|
-
)}
|
|
173
|
-
</ActionRow>
|
|
174
|
-
) : null}
|
|
254
|
+
{actionsContent}
|
|
175
255
|
</AlertDialogContainer>
|
|
176
256
|
</Container>
|
|
177
|
-
)
|
|
257
|
+
), [
|
|
258
|
+
backdropColor,
|
|
259
|
+
closeOnTouchOutside,
|
|
260
|
+
handleCloseButtonPress,
|
|
261
|
+
title,
|
|
262
|
+
containerStyles,
|
|
263
|
+
styles?.titleContainer,
|
|
264
|
+
styles?.bodyContainer,
|
|
265
|
+
titleContent,
|
|
266
|
+
closeButtonContent,
|
|
267
|
+
bodyContent,
|
|
268
|
+
actionsContent,
|
|
269
|
+
]);
|
|
178
270
|
|
|
179
271
|
return (
|
|
180
272
|
// https://github.com/facebook/react-native/issues/48526#issuecomment-2579478884
|
|
181
273
|
<View>
|
|
182
274
|
<Modal
|
|
183
275
|
animationType="fade"
|
|
184
|
-
style={
|
|
185
|
-
css`
|
|
186
|
-
flex: 1;
|
|
187
|
-
align-self: stretch;
|
|
188
|
-
`,
|
|
189
|
-
style,
|
|
190
|
-
]}
|
|
276
|
+
style={modalStyles}
|
|
191
277
|
transparent={true}
|
|
192
278
|
visible={visible}
|
|
193
279
|
>
|
|
194
280
|
{closeOnTouchOutside ? (
|
|
195
281
|
<TouchableWithoutFeedback
|
|
196
|
-
onPress={
|
|
282
|
+
onPress={handleBackdropPress}
|
|
197
283
|
style={css`
|
|
198
284
|
flex: 1;
|
|
199
285
|
`}
|
|
@@ -208,4 +294,5 @@ function AlertDialog(
|
|
|
208
294
|
);
|
|
209
295
|
}
|
|
210
296
|
|
|
211
|
-
|
|
297
|
+
// Export memoized component for better performance
|
|
298
|
+
export default React.memo(forwardRef<AlertDialogContext, AlertDialogProps>(AlertDialog));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useImperativeHandle, useState } from 'react';
|
|
2
|
+
import React, { forwardRef, useImperativeHandle, useState, useCallback, useMemo } from 'react';
|
|
3
3
|
import { Modal, Platform, StyleSheet, View } from 'react-native';
|
|
4
4
|
import styled, { css } from '@emotion/native';
|
|
5
5
|
import { SnackbarTimer } from './const';
|
|
@@ -44,51 +44,75 @@ function Snackbar({ style }, ref) {
|
|
|
44
44
|
const [options, setOptions] = useState(null);
|
|
45
45
|
const [visible, setVisible] = useState(false);
|
|
46
46
|
const { theme } = useTheme();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
47
|
+
// Memoize the close handler
|
|
48
|
+
const handleClose = useCallback(() => {
|
|
49
|
+
setVisible(false);
|
|
50
|
+
setOptions(null);
|
|
51
|
+
}, []);
|
|
52
|
+
// Memoize the open handler
|
|
53
|
+
const handleOpen = useCallback((snackbarOptions) => {
|
|
54
|
+
clearTimer();
|
|
55
|
+
setVisible(true);
|
|
56
|
+
if (snackbarOptions) {
|
|
57
|
+
setOptions(snackbarOptions);
|
|
58
|
+
}
|
|
59
|
+
timer = setTimeout(() => {
|
|
60
60
|
setVisible(false);
|
|
61
|
-
|
|
62
|
-
},
|
|
63
|
-
})
|
|
61
|
+
clearTimer();
|
|
62
|
+
}, snackbarOptions?.timer ?? SnackbarTimer.SHORT);
|
|
63
|
+
}, []);
|
|
64
|
+
useImperativeHandle(ref, () => ({
|
|
65
|
+
open: handleOpen,
|
|
66
|
+
close: handleClose,
|
|
67
|
+
}), [handleOpen, handleClose]);
|
|
64
68
|
const { text, styles, actionText, color = 'primary' } = options ?? {};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
// Memoize shadow styles
|
|
70
|
+
const shadowStyles = useMemo(() => Platform.OS !== 'web' ? {
|
|
71
|
+
shadowOffset: { width: 0, height: 4 },
|
|
72
|
+
shadowColor: theme.text.basic,
|
|
73
|
+
} : {}, [theme.text.basic]);
|
|
74
|
+
// Memoize container styles
|
|
75
|
+
const containerStyles = useMemo(() => StyleSheet.flatten([shadowStyles, styles?.container]), [shadowStyles, styles?.container]);
|
|
76
|
+
// Memoize text styles
|
|
77
|
+
const textStyles = useMemo(() => StyleSheet.flatten([
|
|
78
|
+
css `
|
|
79
|
+
color: ${theme.button[color].text};
|
|
80
|
+
`,
|
|
81
|
+
styles?.text,
|
|
82
|
+
]), [theme.button, color, styles?.text]);
|
|
83
|
+
// Memoize action text styles
|
|
84
|
+
const actionTextStyles = useMemo(() => StyleSheet.flatten([
|
|
85
|
+
css `
|
|
86
|
+
color: ${theme.button[color].text};
|
|
87
|
+
`,
|
|
88
|
+
styles?.actionText,
|
|
89
|
+
]), [theme.button, color, styles?.actionText]);
|
|
90
|
+
// Memoize action button handler
|
|
91
|
+
const handleActionPress = useCallback(() => setVisible(false), []);
|
|
92
|
+
// Memoize modal styles
|
|
93
|
+
const modalStyles = useMemo(() => [
|
|
94
|
+
css `
|
|
95
|
+
flex: 1;
|
|
96
|
+
align-self: stretch;
|
|
97
|
+
`,
|
|
98
|
+
style,
|
|
99
|
+
], [style]);
|
|
100
|
+
const SnackbarContent = useMemo(() => (_jsx(Container, { children: _jsxs(SnackbarContainer, { color: color, style: containerStyles, children: [_jsx(SnackbarText, { color: color, style: textStyles, children: text }), _jsx(ActionContainer, { style: styles?.actionContainer, children: actionText ? (_jsx(Button, { onPress: handleActionPress, styles: {
|
|
101
|
+
text: actionTextStyles,
|
|
102
|
+
}, text: actionText, type: "text" })) : (_jsx(Button, { onPress: handleActionPress, text: _jsx(Icon, { color: theme.button[color].text, name: "X" }), type: "text" })) })] }) })), [
|
|
103
|
+
color,
|
|
104
|
+
containerStyles,
|
|
105
|
+
textStyles,
|
|
106
|
+
text,
|
|
107
|
+
styles?.actionContainer,
|
|
108
|
+
actionText,
|
|
109
|
+
actionTextStyles,
|
|
110
|
+
handleActionPress,
|
|
111
|
+
theme.button,
|
|
112
|
+
]);
|
|
84
113
|
return (
|
|
85
114
|
// https://github.com/facebook/react-native/issues/48526#issuecomment-2579478884
|
|
86
|
-
_jsx(View, { children: _jsx(Modal, { animationType: "fade", style:
|
|
87
|
-
css `
|
|
88
|
-
flex: 1;
|
|
89
|
-
align-self: stretch;
|
|
90
|
-
`,
|
|
91
|
-
style,
|
|
92
|
-
], transparent: true, visible: visible, children: SnackbarContent }) }));
|
|
115
|
+
_jsx(View, { children: _jsx(Modal, { animationType: "fade", style: modalStyles, transparent: true, visible: visible, children: SnackbarContent }) }));
|
|
93
116
|
}
|
|
94
|
-
|
|
117
|
+
// Export memoized component for better performance
|
|
118
|
+
export default React.memo(forwardRef(Snackbar));
|