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.
Files changed (71) hide show
  1. package/README.md +12 -0
  2. package/components/common/ErrorBoundary/ErrorBoundary.js +48 -0
  3. package/components/common/ErrorBoundary/ErrorBoundary.tsx +75 -0
  4. package/components/modals/AlertDialog/AlertDialog.js +79 -37
  5. package/components/modals/AlertDialog/AlertDialog.tsx +152 -65
  6. package/components/modals/Snackbar/Snackbar.js +68 -44
  7. package/components/modals/Snackbar/Snackbar.tsx +93 -49
  8. package/components/uis/Accordion/Accordion.js +8 -4
  9. package/components/uis/Accordion/Accordion.tsx +21 -12
  10. package/components/uis/Accordion/AccordionItem.js +53 -33
  11. package/components/uis/Accordion/AccordionItem.tsx +71 -41
  12. package/components/uis/Button/Button.js +82 -62
  13. package/components/uis/Button/Button.tsx +101 -73
  14. package/components/uis/Card/Card.js +0 -0
  15. package/components/uis/Card/Card.test.tsx +0 -0
  16. package/components/uis/Card/Card.tsx +0 -0
  17. package/components/uis/Checkbox/Checkbox.js +34 -24
  18. package/components/uis/Checkbox/Checkbox.test.tsx +19 -12
  19. package/components/uis/Checkbox/Checkbox.tsx +49 -29
  20. package/components/uis/CustomPressable/CustomPressable.js +20 -13
  21. package/components/uis/CustomPressable/CustomPressable.tsx +32 -16
  22. package/components/uis/EditText/EditText.js +58 -27
  23. package/components/uis/EditText/EditText.test.tsx +14 -9
  24. package/components/uis/EditText/EditText.tsx +51 -26
  25. package/components/uis/Fab/Fab.js +51 -35
  26. package/components/uis/Fab/Fab.tsx +80 -55
  27. package/components/uis/Hr/Hr.js +4 -1
  28. package/components/uis/Hr/Hr.tsx +5 -1
  29. package/components/uis/Icon/Icon.js +5 -2
  30. package/components/uis/Icon/Icon.tsx +7 -3
  31. package/components/uis/IconButton/IconButton.js +84 -63
  32. package/components/uis/IconButton/IconButton.tsx +112 -98
  33. package/components/uis/LoadingIndicator/LoadingIndicator.js +42 -17
  34. package/components/uis/LoadingIndicator/LoadingIndicator.tsx +66 -40
  35. package/components/uis/RadioGroup/RadioButton.js +43 -31
  36. package/components/uis/RadioGroup/RadioButton.tsx +60 -36
  37. package/components/uis/RadioGroup/RadioGroup.js +11 -4
  38. package/components/uis/RadioGroup/RadioGroup.tsx +34 -18
  39. package/components/uis/Rating/Rating.js +35 -21
  40. package/components/uis/Rating/Rating.tsx +128 -92
  41. package/components/uis/StatusbarBrightness/StatusBarBrightness.js +11 -6
  42. package/components/uis/StatusbarBrightness/StatusBarBrightness.tsx +16 -7
  43. package/components/uis/SwitchToggle/SwitchToggle.js +99 -54
  44. package/components/uis/SwitchToggle/SwitchToggle.tsx +165 -103
  45. package/components/uis/Typography/Typography.js +13 -7
  46. package/components/uis/Typography/Typography.tsx +25 -18
  47. package/hooks/index.js +2 -0
  48. package/hooks/index.ts +2 -0
  49. package/hooks/useAnimations.js +47 -0
  50. package/hooks/useAnimations.ts +71 -0
  51. package/hooks/useOptimizedStyles.js +13 -0
  52. package/hooks/useOptimizedStyles.ts +22 -0
  53. package/hooks/usePerformance.js +58 -0
  54. package/hooks/usePerformance.ts +93 -0
  55. package/index.js +8 -0
  56. package/index.tsx +12 -0
  57. package/package.json +1 -1
  58. package/test/testUtils.js +65 -0
  59. package/test/testUtils.tsx +101 -0
  60. package/types/common.js +1 -0
  61. package/types/common.ts +77 -0
  62. package/types/index.js +1 -0
  63. package/types/index.ts +1 -0
  64. package/utils/accessibility.js +79 -0
  65. package/utils/accessibility.ts +110 -0
  66. package/utils/index.js +2 -0
  67. package/utils/index.ts +2 -0
  68. package/utils/performanceTest.js +128 -0
  69. package/utils/performanceTest.ts +174 -0
  70. package/utils/platform.js +59 -0
  71. 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: (alertDialogOptions) => {
50
- setVisible(true);
51
- if (alertDialogOptions) {
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
- const AlertDialogContent = (_jsx(Container, { style: css `
61
- background-color: ${themeType === 'light'
62
- ? `rgba(0,0,0,${backdropOpacity})`
63
- : `rgba(255,255,255,${backdropOpacity})`};
64
- `, children: _jsxs(AlertDialogContainer, { style: StyleSheet.flatten([
65
- Platform.OS !== 'web' && {
66
- shadowOffset: { width: 0, height: 4 },
67
- shadowColor: theme.text.basic,
68
- },
69
- styles?.container,
70
- ]), children: [_jsxs(TitleRow, { style: styles?.titleContainer, children: [typeof title === 'string' ? (_jsx(Typography.Heading3, { style: styles?.title, children: title })) : (title), showCloseButton ? (_jsx(Button, { onPress: () => setVisible(false), borderRadius: 24, text: _jsx(Icon, { color: theme.text.basic, name: "X", size: 18 }), type: "text" })) : null] }), _jsx(BodyRow, { style: styles?.bodyContainer, children: typeof body === 'string' ? (_jsx(Typography.Body3, { style: styles?.body, children: body })) : (body) }), actions ? (_jsx(ActionRow, { style: styles?.actionContainer, children: actions.map((action, index) => cloneElement(action, {
71
- key: `action-${index}`,
72
- style: {
73
- flex: 1,
74
- marginLeft: index !== 0 ? 12 : 0,
75
- },
76
- })) })) : null] }) }));
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
- export default forwardRef(AlertDialog);
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: (alertDialogOptions) => {
101
- setVisible(true);
102
- if (alertDialogOptions) {
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
- const AlertDialogContent = (
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: ${themeType === 'light'
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
- style={StyleSheet.flatten([
131
- Platform.OS !== 'web' && {
132
- shadowOffset: {width: 0, height: 4},
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
- {typeof title === 'string' ? (
140
- <Typography.Heading3 style={styles?.title}>
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
- {typeof body === 'string' ? (
157
- <Typography.Body3 style={styles?.body}>{body}</Typography.Body3>
158
- ) : (
159
- body
160
- )}
252
+ {bodyContent}
161
253
  </BodyRow>
162
- {actions ? (
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={() => setVisible(false)}
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
- export default forwardRef<AlertDialogContext, AlertDialogProps>(AlertDialog);
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
- useImperativeHandle(ref, () => ({
48
- open: (snackbarOptions) => {
49
- clearTimer();
50
- setVisible(true);
51
- if (snackbarOptions) {
52
- setOptions(snackbarOptions);
53
- }
54
- timer = setTimeout(() => {
55
- setVisible(false);
56
- clearTimer();
57
- }, snackbarOptions?.timer ?? SnackbarTimer.SHORT);
58
- },
59
- close: () => {
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
- setOptions(null);
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
- const SnackbarContent = (_jsx(Container, { children: _jsxs(SnackbarContainer, { color: color, style: StyleSheet.flatten([
66
- Platform.OS !== 'web' && {
67
- shadowOffset: { width: 0, height: 4 },
68
- shadowColor: theme.text.basic,
69
- },
70
- styles?.container,
71
- ]), children: [_jsx(SnackbarText, { color: color, style: StyleSheet.flatten([
72
- css `
73
- color: ${theme.button[color].text};
74
- `,
75
- styles?.text,
76
- ]), children: text }), _jsx(ActionContainer, { style: styles?.actionContainer, children: actionText ? (_jsx(Button, { onPress: () => setVisible(false), styles: {
77
- text: StyleSheet.flatten([
78
- css `
79
- color: ${theme.button[color].text};
80
- `,
81
- styles?.actionText,
82
- ]),
83
- }, text: actionText, type: "text" })) : (_jsx(Button, { onPress: () => setVisible(false), text: _jsx(Icon, { color: theme.button[color].text, name: "X" }), type: "text" })) })] }) }));
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
- export default forwardRef(Snackbar);
117
+ // Export memoized component for better performance
118
+ export default React.memo(forwardRef(Snackbar));