@idealyst/components 1.0.41 → 1.0.44
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/CLAUDE.md +57 -3
- package/package.json +2 -2
- package/src/Dialog/Dialog.native.tsx +91 -0
- package/src/Dialog/Dialog.styles.tsx +148 -0
- package/src/Dialog/Dialog.web.tsx +170 -0
- package/src/Dialog/README.md +210 -0
- package/src/Dialog/index.native.ts +2 -0
- package/src/Dialog/index.ts +2 -0
- package/src/Dialog/index.web.ts +2 -0
- package/src/Dialog/types.ts +63 -0
- package/src/Input/Input.native.tsx +12 -3
- package/src/Input/Input.styles.tsx +23 -0
- package/src/Input/Input.web.tsx +34 -6
- package/src/Input/types.ts +11 -1
- package/src/Popover/Popover.native.tsx +87 -0
- package/src/Popover/Popover.styles.tsx +96 -0
- package/src/Popover/Popover.web.tsx +287 -0
- package/src/Popover/index.native.ts +2 -0
- package/src/Popover/index.ts +2 -0
- package/src/Popover/index.web.ts +2 -0
- package/src/Popover/types.ts +65 -0
- package/src/examples/AllExamples.tsx +8 -0
- package/src/examples/DialogExamples.tsx +157 -0
- package/src/examples/PopoverExamples.tsx +155 -0
- package/src/examples/index.ts +2 -0
- package/src/index.native.ts +9 -0
- package/src/index.ts +8 -0
package/CLAUDE.md
CHANGED
|
@@ -5,7 +5,7 @@ This file provides comprehensive component documentation for LLMs working with t
|
|
|
5
5
|
## Library Overview
|
|
6
6
|
|
|
7
7
|
@idealyst/components is a cross-platform React/React Native component library with:
|
|
8
|
-
-
|
|
8
|
+
- 13 core components organized into 6 categories
|
|
9
9
|
- Theme-based styling with Unistyles
|
|
10
10
|
- Intent-based color system (primary, neutral, success, error, warning)
|
|
11
11
|
- Cross-platform compatibility (React & React Native)
|
|
@@ -34,6 +34,10 @@ This file provides comprehensive component documentation for LLMs working with t
|
|
|
34
34
|
### Utility Components
|
|
35
35
|
- **Icon**: Icon display (`name`, `size`, `color`, `intent`)
|
|
36
36
|
|
|
37
|
+
### Overlay Components
|
|
38
|
+
- **Dialog**: Modal dialog (`open`, `onOpenChange`, `title`, `size="small|medium|large"`, `variant="default"`, `showCloseButton`, `closeOnBackdropClick`, `closeOnEscapeKey`)
|
|
39
|
+
- **Popover**: Contextual overlay (`open`, `onOpenChange`, `anchor`, `placement="top|bottom|left|right"`, `offset`, `closeOnClickOutside`, `closeOnEscapeKey`, `showArrow`)
|
|
40
|
+
|
|
37
41
|
## Intent System
|
|
38
42
|
|
|
39
43
|
All components use a consistent intent-based color system:
|
|
@@ -73,6 +77,54 @@ All components use a consistent intent-based color system:
|
|
|
73
77
|
</Card>
|
|
74
78
|
```
|
|
75
79
|
|
|
80
|
+
### Dialog Usage
|
|
81
|
+
```tsx
|
|
82
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
83
|
+
|
|
84
|
+
<Dialog
|
|
85
|
+
open={dialogOpen}
|
|
86
|
+
onOpenChange={setDialogOpen}
|
|
87
|
+
title="Confirm Action"
|
|
88
|
+
size="medium"
|
|
89
|
+
>
|
|
90
|
+
<View spacing="md">
|
|
91
|
+
<Text>Are you sure you want to proceed?</Text>
|
|
92
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
93
|
+
<Button variant="outlined" onPress={() => setDialogOpen(false)}>
|
|
94
|
+
Cancel
|
|
95
|
+
</Button>
|
|
96
|
+
<Button variant="contained" intent="primary" onPress={handleConfirm}>
|
|
97
|
+
Confirm
|
|
98
|
+
</Button>
|
|
99
|
+
</View>
|
|
100
|
+
</View>
|
|
101
|
+
</Dialog>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Popover Usage
|
|
105
|
+
```tsx
|
|
106
|
+
const [popoverOpen, setPopoverOpen] = useState(false);
|
|
107
|
+
const buttonRef = useRef<HTMLDivElement>(null);
|
|
108
|
+
|
|
109
|
+
<div ref={buttonRef} style={{ display: 'inline-block' }}>
|
|
110
|
+
<Button onPress={() => setPopoverOpen(true)}>
|
|
111
|
+
Show Menu
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
<Popover
|
|
115
|
+
open={popoverOpen}
|
|
116
|
+
onOpenChange={setPopoverOpen}
|
|
117
|
+
anchor={buttonRef}
|
|
118
|
+
placement="bottom-start"
|
|
119
|
+
showArrow
|
|
120
|
+
>
|
|
121
|
+
<View spacing="sm">
|
|
122
|
+
<Button variant="text" onPress={handleAction}>Action 1</Button>
|
|
123
|
+
<Button variant="text" onPress={handleAction}>Action 2</Button>
|
|
124
|
+
</View>
|
|
125
|
+
</Popover>
|
|
126
|
+
```
|
|
127
|
+
|
|
76
128
|
## Styling Guidelines
|
|
77
129
|
|
|
78
130
|
1. **Use variants over manual styles** - Components provide semantic variants
|
|
@@ -85,13 +137,13 @@ All components use a consistent intent-based color system:
|
|
|
85
137
|
|
|
86
138
|
```tsx
|
|
87
139
|
// Individual imports (recommended)
|
|
88
|
-
import { Button, Text, View } from '@idealyst/components';
|
|
140
|
+
import { Button, Text, View, Dialog, Popover } from '@idealyst/components';
|
|
89
141
|
|
|
90
142
|
// Documentation access
|
|
91
143
|
import { componentDocs, getComponentDocs } from '@idealyst/components/docs';
|
|
92
144
|
|
|
93
145
|
// Examples
|
|
94
|
-
import { ButtonExamples } from '@idealyst/components/examples';
|
|
146
|
+
import { ButtonExamples, DialogExamples, PopoverExamples } from '@idealyst/components/examples';
|
|
95
147
|
```
|
|
96
148
|
|
|
97
149
|
## Key Props Reference
|
|
@@ -144,9 +196,11 @@ src/Badge/README.md
|
|
|
144
196
|
src/Button/README.md
|
|
145
197
|
src/Card/README.md
|
|
146
198
|
src/Checkbox/README.md
|
|
199
|
+
src/Dialog/README.md
|
|
147
200
|
src/Divider/README.md
|
|
148
201
|
src/Icon/README.md
|
|
149
202
|
src/Input/README.md
|
|
203
|
+
src/Popover/README.md
|
|
150
204
|
src/Screen/README.md
|
|
151
205
|
src/Text/README.md
|
|
152
206
|
src/View/README.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.44",
|
|
4
4
|
"description": "Shared component library for React and React Native",
|
|
5
5
|
"documentation": "https://github.com/your-username/idealyst-framework/tree/main/packages/components#readme",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"publish:npm": "npm publish"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@idealyst/theme": "^1.0.
|
|
43
|
+
"@idealyst/theme": "^1.0.44",
|
|
44
44
|
"@mdi/js": "^7.4.47",
|
|
45
45
|
"@mdi/react": "^1.6.1",
|
|
46
46
|
"@react-native-vector-icons/common": "^12.0.1",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Modal, View, Text, TouchableOpacity, TouchableWithoutFeedback, BackHandler } from 'react-native';
|
|
3
|
+
import { DialogProps } from './types';
|
|
4
|
+
import { dialogStyles } from './Dialog.styles';
|
|
5
|
+
|
|
6
|
+
const Dialog: React.FC<DialogProps> = ({
|
|
7
|
+
open,
|
|
8
|
+
onOpenChange,
|
|
9
|
+
title,
|
|
10
|
+
children,
|
|
11
|
+
size = 'medium',
|
|
12
|
+
variant = 'default',
|
|
13
|
+
showCloseButton = true,
|
|
14
|
+
closeOnBackdropClick = true,
|
|
15
|
+
animationType = 'fade',
|
|
16
|
+
style,
|
|
17
|
+
testID,
|
|
18
|
+
}) => {
|
|
19
|
+
// Handle Android back button
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!open) return;
|
|
22
|
+
|
|
23
|
+
const handleBackPress = () => {
|
|
24
|
+
onOpenChange(false);
|
|
25
|
+
return true; // Prevent default back behavior
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
|
29
|
+
return () => backHandler.remove();
|
|
30
|
+
}, [open, onOpenChange]);
|
|
31
|
+
|
|
32
|
+
const handleBackdropPress = () => {
|
|
33
|
+
if (closeOnBackdropClick) {
|
|
34
|
+
onOpenChange(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleClosePress = () => {
|
|
39
|
+
onOpenChange(false);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Apply variants
|
|
43
|
+
dialogStyles.useVariants({
|
|
44
|
+
size,
|
|
45
|
+
variant,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Modal
|
|
50
|
+
visible={open}
|
|
51
|
+
transparent
|
|
52
|
+
animationType={animationType}
|
|
53
|
+
onRequestClose={() => onOpenChange(false)}
|
|
54
|
+
statusBarTranslucent
|
|
55
|
+
testID={testID}
|
|
56
|
+
>
|
|
57
|
+
<TouchableWithoutFeedback onPress={handleBackdropPress}>
|
|
58
|
+
<View style={dialogStyles.backdrop}>
|
|
59
|
+
<TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
|
|
60
|
+
<View style={[dialogStyles.container, style]}>
|
|
61
|
+
{(title || showCloseButton) && (
|
|
62
|
+
<View style={dialogStyles.header}>
|
|
63
|
+
{title && (
|
|
64
|
+
<Text style={dialogStyles.title}>
|
|
65
|
+
{title}
|
|
66
|
+
</Text>
|
|
67
|
+
)}
|
|
68
|
+
{showCloseButton && (
|
|
69
|
+
<TouchableOpacity
|
|
70
|
+
style={dialogStyles.closeButton}
|
|
71
|
+
onPress={handleClosePress}
|
|
72
|
+
accessibilityLabel="Close dialog"
|
|
73
|
+
accessibilityRole="button"
|
|
74
|
+
>
|
|
75
|
+
<Text style={dialogStyles.closeButtonText}>×</Text>
|
|
76
|
+
</TouchableOpacity>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
)}
|
|
80
|
+
<View style={dialogStyles.content}>
|
|
81
|
+
{children}
|
|
82
|
+
</View>
|
|
83
|
+
</View>
|
|
84
|
+
</TouchableWithoutFeedback>
|
|
85
|
+
</View>
|
|
86
|
+
</TouchableWithoutFeedback>
|
|
87
|
+
</Modal>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default Dialog;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const dialogStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
backdrop: {
|
|
5
|
+
position: 'absolute',
|
|
6
|
+
top: 0,
|
|
7
|
+
left: 0,
|
|
8
|
+
right: 0,
|
|
9
|
+
bottom: 0,
|
|
10
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
11
|
+
display: 'flex',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
zIndex: 1000,
|
|
15
|
+
|
|
16
|
+
// Web-specific styles
|
|
17
|
+
_web: {
|
|
18
|
+
position: 'fixed',
|
|
19
|
+
transition: 'opacity 150ms ease-out',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
container: {
|
|
24
|
+
backgroundColor: theme.colors?.background?.primary || '#ffffff',
|
|
25
|
+
borderRadius: theme.borderRadius?.lg || 12,
|
|
26
|
+
shadowColor: '#000',
|
|
27
|
+
shadowOffset: {
|
|
28
|
+
width: 0,
|
|
29
|
+
height: 10,
|
|
30
|
+
},
|
|
31
|
+
shadowOpacity: 0.25,
|
|
32
|
+
shadowRadius: 20,
|
|
33
|
+
elevation: 10,
|
|
34
|
+
maxHeight: '90%',
|
|
35
|
+
|
|
36
|
+
variants: {
|
|
37
|
+
size: {
|
|
38
|
+
small: {
|
|
39
|
+
width: '90%',
|
|
40
|
+
maxWidth: 400,
|
|
41
|
+
},
|
|
42
|
+
medium: {
|
|
43
|
+
width: '90%',
|
|
44
|
+
maxWidth: 600,
|
|
45
|
+
},
|
|
46
|
+
large: {
|
|
47
|
+
width: '90%',
|
|
48
|
+
maxWidth: 800,
|
|
49
|
+
},
|
|
50
|
+
fullscreen: {
|
|
51
|
+
width: '100%',
|
|
52
|
+
height: '100%',
|
|
53
|
+
borderRadius: 0,
|
|
54
|
+
maxHeight: '100%',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
variant: {
|
|
58
|
+
default: {},
|
|
59
|
+
alert: {
|
|
60
|
+
borderTopWidth: 4,
|
|
61
|
+
borderTopColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
62
|
+
},
|
|
63
|
+
confirmation: {
|
|
64
|
+
borderTopWidth: 4,
|
|
65
|
+
borderTopColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Web-specific styles
|
|
71
|
+
_web: {
|
|
72
|
+
position: 'relative',
|
|
73
|
+
display: 'flex',
|
|
74
|
+
flexDirection: 'column',
|
|
75
|
+
overflow: 'auto',
|
|
76
|
+
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
|
77
|
+
transition: 'opacity 150ms ease-out, transform 150ms ease-out',
|
|
78
|
+
transformOrigin: 'center center',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
header: {
|
|
83
|
+
borderBottomWidth: 1,
|
|
84
|
+
borderBottomColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
85
|
+
display: 'flex',
|
|
86
|
+
flexDirection: 'row',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
justifyContent: 'space-between',
|
|
89
|
+
|
|
90
|
+
_web: {
|
|
91
|
+
borderBottomStyle: 'solid',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
title: {
|
|
96
|
+
marginLeft: theme.spacing?.lg || 12,
|
|
97
|
+
fontSize: 18,
|
|
98
|
+
paddingVertical: theme.spacing.md,
|
|
99
|
+
fontWeight: '600',
|
|
100
|
+
color: theme.colors?.text?.primary || '#111827',
|
|
101
|
+
flex: 1,
|
|
102
|
+
|
|
103
|
+
_web: {
|
|
104
|
+
paddingVertical: theme.spacing.xs,
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
closeButton: {
|
|
109
|
+
width: 32,
|
|
110
|
+
height: 32,
|
|
111
|
+
marginRight: theme.spacing?.md || 12,
|
|
112
|
+
borderRadius: 16,
|
|
113
|
+
backgroundColor: 'transparent',
|
|
114
|
+
border: 'none',
|
|
115
|
+
display: 'flex',
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
justifyContent: 'center',
|
|
118
|
+
cursor: 'pointer',
|
|
119
|
+
|
|
120
|
+
_web: {
|
|
121
|
+
_hover: {
|
|
122
|
+
backgroundColor: theme.colors?.background?.secondary || '#f3f4f6',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
closeButtonText: {
|
|
128
|
+
fontSize: 18,
|
|
129
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
130
|
+
fontWeight: '500',
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
content: {
|
|
134
|
+
padding: theme.spacing?.lg || 16,
|
|
135
|
+
|
|
136
|
+
_web: {
|
|
137
|
+
overflow: 'visible',
|
|
138
|
+
maxHeight: 'none',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Native-specific modal styles
|
|
143
|
+
modal: {
|
|
144
|
+
margin: 0,
|
|
145
|
+
justifyContent: 'center',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
4
|
+
import { DialogProps } from './types';
|
|
5
|
+
import { dialogStyles } from './Dialog.styles';
|
|
6
|
+
import Icon from '../Icon';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const Dialog: React.FC<DialogProps> = ({
|
|
10
|
+
open,
|
|
11
|
+
onOpenChange,
|
|
12
|
+
title,
|
|
13
|
+
children,
|
|
14
|
+
size = 'medium',
|
|
15
|
+
variant = 'default',
|
|
16
|
+
showCloseButton = true,
|
|
17
|
+
closeOnBackdropClick = true,
|
|
18
|
+
closeOnEscapeKey = true,
|
|
19
|
+
style,
|
|
20
|
+
testID,
|
|
21
|
+
}) => {
|
|
22
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const previousActiveElementRef = useRef<HTMLElement | null>(null);
|
|
24
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
25
|
+
const [shouldRender, setShouldRender] = useState(false);
|
|
26
|
+
|
|
27
|
+
// Handle mounting/unmounting with animation
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (open && !shouldRender) {
|
|
30
|
+
// Opening sequence
|
|
31
|
+
setShouldRender(true);
|
|
32
|
+
// Use double requestAnimationFrame to ensure the DOM has fully rendered
|
|
33
|
+
requestAnimationFrame(() => {
|
|
34
|
+
requestAnimationFrame(() => {
|
|
35
|
+
setIsVisible(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
} else if (!open && shouldRender) {
|
|
39
|
+
// Closing sequence
|
|
40
|
+
setIsVisible(false);
|
|
41
|
+
// Wait for transition to complete before unmounting
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
setShouldRender(false);
|
|
44
|
+
}, 150); // Match transition duration
|
|
45
|
+
return () => clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}, [open, shouldRender]);
|
|
48
|
+
|
|
49
|
+
// Handle escape key
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!open || !closeOnEscapeKey) return;
|
|
52
|
+
|
|
53
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
54
|
+
if (event.key === 'Escape') {
|
|
55
|
+
onOpenChange(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
document.addEventListener('keydown', handleEscape);
|
|
60
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
61
|
+
}, [open, closeOnEscapeKey, onOpenChange]);
|
|
62
|
+
|
|
63
|
+
// Handle focus management
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (open) {
|
|
66
|
+
// Store the currently focused element
|
|
67
|
+
previousActiveElementRef.current = document.activeElement as HTMLElement;
|
|
68
|
+
|
|
69
|
+
// Focus the dialog
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
dialogRef.current?.focus();
|
|
72
|
+
}, 0);
|
|
73
|
+
} else {
|
|
74
|
+
// Restore focus to the previously focused element
|
|
75
|
+
if (previousActiveElementRef.current) {
|
|
76
|
+
previousActiveElementRef.current.focus();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, [open]);
|
|
80
|
+
|
|
81
|
+
// Prevent body scroll when dialog is open
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (open) {
|
|
84
|
+
const originalStyle = window.getComputedStyle(document.body).overflow;
|
|
85
|
+
document.body.style.overflow = 'hidden';
|
|
86
|
+
return () => {
|
|
87
|
+
document.body.style.overflow = originalStyle;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}, [open]);
|
|
91
|
+
|
|
92
|
+
if (!shouldRender) return null;
|
|
93
|
+
|
|
94
|
+
const handleBackdropClick = (event: React.MouseEvent) => {
|
|
95
|
+
if (closeOnBackdropClick && event.target === event.currentTarget) {
|
|
96
|
+
onOpenChange(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleCloseClick = () => {
|
|
101
|
+
onOpenChange(false);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Apply variants
|
|
105
|
+
dialogStyles.useVariants({
|
|
106
|
+
size,
|
|
107
|
+
variant,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const backdropProps = getWebProps([
|
|
111
|
+
dialogStyles.backdrop,
|
|
112
|
+
{ opacity: isVisible ? 1 : 0 }
|
|
113
|
+
]);
|
|
114
|
+
const containerProps = getWebProps([
|
|
115
|
+
dialogStyles.container,
|
|
116
|
+
style,
|
|
117
|
+
isVisible
|
|
118
|
+
? { opacity: 1, transform: 'scale(1) translateY(0px)' }
|
|
119
|
+
: { opacity: 0, transform: 'scale(0.96) translateY(-4px)' }
|
|
120
|
+
]);
|
|
121
|
+
const headerProps = getWebProps([dialogStyles.header]);
|
|
122
|
+
const titleProps = getWebProps([dialogStyles.title]);
|
|
123
|
+
const closeButtonProps = getWebProps([dialogStyles.closeButton]);
|
|
124
|
+
const contentProps = getWebProps([dialogStyles.content]);
|
|
125
|
+
|
|
126
|
+
const dialogContent = (
|
|
127
|
+
<div
|
|
128
|
+
{...backdropProps}
|
|
129
|
+
onClick={handleBackdropClick}
|
|
130
|
+
data-testid={testID}
|
|
131
|
+
>
|
|
132
|
+
<div
|
|
133
|
+
{...containerProps}
|
|
134
|
+
ref={dialogRef}
|
|
135
|
+
role="dialog"
|
|
136
|
+
aria-modal="true"
|
|
137
|
+
aria-labelledby={title ? 'dialog-title' : undefined}
|
|
138
|
+
tabIndex={-1}
|
|
139
|
+
onClick={(e) => e.stopPropagation()}
|
|
140
|
+
>
|
|
141
|
+
{(title || showCloseButton) && (
|
|
142
|
+
<div {...headerProps}>
|
|
143
|
+
{title && (
|
|
144
|
+
<h2 {...titleProps} id="dialog-title">
|
|
145
|
+
{title}
|
|
146
|
+
</h2>
|
|
147
|
+
)}
|
|
148
|
+
{showCloseButton && (
|
|
149
|
+
<button
|
|
150
|
+
{...closeButtonProps}
|
|
151
|
+
onClick={handleCloseClick}
|
|
152
|
+
aria-label="Close dialog"
|
|
153
|
+
type="button"
|
|
154
|
+
>
|
|
155
|
+
<Icon name="close" />
|
|
156
|
+
</button>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
<div {...contentProps}>
|
|
161
|
+
{children}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return createPortal(dialogContent, document.body);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default Dialog;
|