@idealyst/components 1.0.41 → 1.0.43
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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
4
|
+
import { PopoverProps, PopoverPlacement } from './types';
|
|
5
|
+
import { popoverStyles } from './Popover.styles';
|
|
6
|
+
|
|
7
|
+
interface PopoverPosition {
|
|
8
|
+
top: number;
|
|
9
|
+
left: number;
|
|
10
|
+
placement: PopoverPlacement;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const calculatePosition = (
|
|
14
|
+
anchorRect: DOMRect,
|
|
15
|
+
popoverSize: { width: number; height: number },
|
|
16
|
+
placement: PopoverPlacement,
|
|
17
|
+
offset: number,
|
|
18
|
+
showArrow: boolean = false
|
|
19
|
+
): PopoverPosition => {
|
|
20
|
+
const viewport = {
|
|
21
|
+
width: window.innerWidth,
|
|
22
|
+
height: window.innerHeight,
|
|
23
|
+
scrollX: window.scrollX,
|
|
24
|
+
scrollY: window.scrollY,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let position = { top: 0, left: 0 };
|
|
28
|
+
let finalPlacement = placement;
|
|
29
|
+
|
|
30
|
+
// Add extra offset for arrow
|
|
31
|
+
const arrowSize = 6;
|
|
32
|
+
const finalOffset = showArrow ? offset + arrowSize : offset;
|
|
33
|
+
|
|
34
|
+
// Calculate initial position based on placement
|
|
35
|
+
switch (placement) {
|
|
36
|
+
case 'top':
|
|
37
|
+
position = {
|
|
38
|
+
top: anchorRect.top + viewport.scrollY - popoverSize.height - finalOffset,
|
|
39
|
+
left: anchorRect.left + viewport.scrollX + anchorRect.width / 2 - popoverSize.width / 2,
|
|
40
|
+
};
|
|
41
|
+
break;
|
|
42
|
+
case 'top-start':
|
|
43
|
+
position = {
|
|
44
|
+
top: anchorRect.top + viewport.scrollY - popoverSize.height - finalOffset,
|
|
45
|
+
left: anchorRect.left + viewport.scrollX,
|
|
46
|
+
};
|
|
47
|
+
break;
|
|
48
|
+
case 'top-end':
|
|
49
|
+
position = {
|
|
50
|
+
top: anchorRect.top + viewport.scrollY - popoverSize.height - finalOffset,
|
|
51
|
+
left: anchorRect.right + viewport.scrollX - popoverSize.width,
|
|
52
|
+
};
|
|
53
|
+
break;
|
|
54
|
+
case 'bottom':
|
|
55
|
+
position = {
|
|
56
|
+
top: anchorRect.bottom + viewport.scrollY + finalOffset,
|
|
57
|
+
left: anchorRect.left + viewport.scrollX + anchorRect.width / 2 - popoverSize.width / 2,
|
|
58
|
+
};
|
|
59
|
+
break;
|
|
60
|
+
case 'bottom-start':
|
|
61
|
+
position = {
|
|
62
|
+
top: anchorRect.bottom + viewport.scrollY + finalOffset,
|
|
63
|
+
left: anchorRect.left + viewport.scrollX,
|
|
64
|
+
};
|
|
65
|
+
break;
|
|
66
|
+
case 'bottom-end':
|
|
67
|
+
position = {
|
|
68
|
+
top: anchorRect.bottom + viewport.scrollY + finalOffset,
|
|
69
|
+
left: anchorRect.right + viewport.scrollX - popoverSize.width,
|
|
70
|
+
};
|
|
71
|
+
break;
|
|
72
|
+
case 'left':
|
|
73
|
+
position = {
|
|
74
|
+
top: anchorRect.top + viewport.scrollY + anchorRect.height / 2 - popoverSize.height / 2,
|
|
75
|
+
left: anchorRect.left + viewport.scrollX - popoverSize.width - finalOffset,
|
|
76
|
+
};
|
|
77
|
+
break;
|
|
78
|
+
case 'left-start':
|
|
79
|
+
position = {
|
|
80
|
+
top: anchorRect.top + viewport.scrollY,
|
|
81
|
+
left: anchorRect.left + viewport.scrollX - popoverSize.width - finalOffset,
|
|
82
|
+
};
|
|
83
|
+
break;
|
|
84
|
+
case 'left-end':
|
|
85
|
+
position = {
|
|
86
|
+
top: anchorRect.bottom + viewport.scrollY - popoverSize.height,
|
|
87
|
+
left: anchorRect.left + viewport.scrollX - popoverSize.width - finalOffset,
|
|
88
|
+
};
|
|
89
|
+
break;
|
|
90
|
+
case 'right':
|
|
91
|
+
position = {
|
|
92
|
+
top: anchorRect.top + viewport.scrollY + anchorRect.height / 2 - popoverSize.height / 2,
|
|
93
|
+
left: anchorRect.right + viewport.scrollX + finalOffset,
|
|
94
|
+
};
|
|
95
|
+
break;
|
|
96
|
+
case 'right-start':
|
|
97
|
+
position = {
|
|
98
|
+
top: anchorRect.top + viewport.scrollY,
|
|
99
|
+
left: anchorRect.right + viewport.scrollX + finalOffset,
|
|
100
|
+
};
|
|
101
|
+
break;
|
|
102
|
+
case 'right-end':
|
|
103
|
+
position = {
|
|
104
|
+
top: anchorRect.bottom + viewport.scrollY - popoverSize.height,
|
|
105
|
+
left: anchorRect.right + viewport.scrollX + finalOffset,
|
|
106
|
+
};
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Constrain to viewport
|
|
111
|
+
const padding = 8;
|
|
112
|
+
position.left = Math.max(padding, Math.min(position.left, viewport.width - popoverSize.width - padding));
|
|
113
|
+
position.top = Math.max(padding, Math.min(position.top, viewport.height + viewport.scrollY - popoverSize.height - padding));
|
|
114
|
+
|
|
115
|
+
return { ...position, placement: finalPlacement };
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const Popover: React.FC<PopoverProps> = ({
|
|
119
|
+
open,
|
|
120
|
+
onOpenChange,
|
|
121
|
+
anchor,
|
|
122
|
+
children,
|
|
123
|
+
placement = 'bottom',
|
|
124
|
+
offset = 8,
|
|
125
|
+
closeOnClickOutside = true,
|
|
126
|
+
closeOnEscapeKey = true,
|
|
127
|
+
showArrow = false,
|
|
128
|
+
testID,
|
|
129
|
+
}) => {
|
|
130
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
131
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
132
|
+
const [shouldRender, setShouldRender] = useState(false);
|
|
133
|
+
const [position, setPosition] = useState<PopoverPosition>({ top: 0, left: 0, placement });
|
|
134
|
+
|
|
135
|
+
// Calculate position
|
|
136
|
+
const updatePosition = useCallback(() => {
|
|
137
|
+
if (!popoverRef.current) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let anchorElement: Element | null = null;
|
|
142
|
+
|
|
143
|
+
if (anchor && typeof anchor === 'object' && 'current' in anchor && anchor.current) {
|
|
144
|
+
anchorElement = anchor.current;
|
|
145
|
+
} else if (React.isValidElement(anchor)) {
|
|
146
|
+
console.warn('Popover: React element anchors need to be refs for positioning');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!anchorElement) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const anchorRect = anchorElement.getBoundingClientRect();
|
|
155
|
+
const popoverRect = popoverRef.current.getBoundingClientRect();
|
|
156
|
+
|
|
157
|
+
const newPosition = calculatePosition(
|
|
158
|
+
anchorRect,
|
|
159
|
+
{ width: popoverRect.width || 200, height: popoverRect.height || 100 },
|
|
160
|
+
placement,
|
|
161
|
+
offset,
|
|
162
|
+
showArrow
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
setPosition(newPosition);
|
|
166
|
+
}, [anchor, placement, offset, showArrow]);
|
|
167
|
+
|
|
168
|
+
// Handle mounting/unmounting with animation
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (open && !shouldRender) {
|
|
171
|
+
setShouldRender(true);
|
|
172
|
+
// Set visible immediately to render the DOM element
|
|
173
|
+
setIsVisible(true);
|
|
174
|
+
} else if (!open && shouldRender) {
|
|
175
|
+
setIsVisible(false);
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
setShouldRender(false);
|
|
178
|
+
}, 150);
|
|
179
|
+
return () => clearTimeout(timer);
|
|
180
|
+
}
|
|
181
|
+
}, [open, shouldRender]);
|
|
182
|
+
|
|
183
|
+
// Position calculation after DOM is ready
|
|
184
|
+
useLayoutEffect(() => {
|
|
185
|
+
if (shouldRender && isVisible) {
|
|
186
|
+
// Use a microtask to ensure the ref is attached
|
|
187
|
+
Promise.resolve().then(() => {
|
|
188
|
+
if (popoverRef.current) {
|
|
189
|
+
updatePosition();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}, [shouldRender, isVisible, anchor, placement, offset, showArrow]);
|
|
194
|
+
|
|
195
|
+
// Update position on scroll/resize
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (shouldRender && isVisible) {
|
|
198
|
+
const handleResize = () => updatePosition();
|
|
199
|
+
const handleScroll = () => updatePosition();
|
|
200
|
+
|
|
201
|
+
window.addEventListener('resize', handleResize);
|
|
202
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
203
|
+
|
|
204
|
+
return () => {
|
|
205
|
+
window.removeEventListener('resize', handleResize);
|
|
206
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}, [updatePosition, shouldRender, isVisible]);
|
|
210
|
+
|
|
211
|
+
// Handle escape key
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (!open || !closeOnEscapeKey) return;
|
|
214
|
+
|
|
215
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
216
|
+
if (event.key === 'Escape') {
|
|
217
|
+
onOpenChange(false);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
document.addEventListener('keydown', handleEscape);
|
|
222
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
223
|
+
}, [open, closeOnEscapeKey, onOpenChange]);
|
|
224
|
+
|
|
225
|
+
// Handle click outside
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (!open || !closeOnClickOutside) return;
|
|
228
|
+
|
|
229
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
230
|
+
if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
|
|
231
|
+
// Check if click was on anchor element
|
|
232
|
+
let anchorElement: Element | null = null;
|
|
233
|
+
if (anchor && typeof anchor === 'object' && 'current' in anchor && anchor.current) {
|
|
234
|
+
anchorElement = anchor.current;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (anchorElement && anchorElement.contains(event.target as Node)) {
|
|
238
|
+
return; // Don't close if clicked on anchor
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
onOpenChange(false);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
246
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
247
|
+
}, [open, closeOnClickOutside, onOpenChange, anchor]);
|
|
248
|
+
|
|
249
|
+
if (!shouldRender) return null;
|
|
250
|
+
|
|
251
|
+
// Use Unistyles with wrapper approach
|
|
252
|
+
popoverStyles.useVariants({});
|
|
253
|
+
|
|
254
|
+
const containerProps = getWebProps([
|
|
255
|
+
popoverStyles.container,
|
|
256
|
+
{
|
|
257
|
+
opacity: isVisible ? 1 : 0,
|
|
258
|
+
transform: isVisible ? 'scale(1)' : 'scale(0.95)',
|
|
259
|
+
}
|
|
260
|
+
]);
|
|
261
|
+
const contentProps = getWebProps([popoverStyles.content]);
|
|
262
|
+
|
|
263
|
+
console.log(position)
|
|
264
|
+
|
|
265
|
+
const popoverContent = (
|
|
266
|
+
<div
|
|
267
|
+
ref={popoverRef}
|
|
268
|
+
style={{
|
|
269
|
+
position: 'fixed',
|
|
270
|
+
zIndex: 9999,
|
|
271
|
+
top: position.top,
|
|
272
|
+
left: position.left,
|
|
273
|
+
}}
|
|
274
|
+
data-testid={testID}
|
|
275
|
+
>
|
|
276
|
+
<div {...containerProps}>
|
|
277
|
+
<div {...contentProps}>
|
|
278
|
+
{children}
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
return createPortal(popoverContent, document.body);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export default Popover;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type PopoverPlacement =
|
|
4
|
+
| 'top' | 'top-start' | 'top-end'
|
|
5
|
+
| 'bottom' | 'bottom-start' | 'bottom-end'
|
|
6
|
+
| 'left' | 'left-start' | 'left-end'
|
|
7
|
+
| 'right' | 'right-start' | 'right-end';
|
|
8
|
+
|
|
9
|
+
export interface PopoverProps {
|
|
10
|
+
/**
|
|
11
|
+
* Whether the popover is open/visible
|
|
12
|
+
*/
|
|
13
|
+
open: boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Called when the popover should be opened or closed
|
|
17
|
+
*/
|
|
18
|
+
onOpenChange: (open: boolean) => void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The anchor element to position the popover relative to
|
|
22
|
+
* Can be a React element or a ref to a DOM element
|
|
23
|
+
*/
|
|
24
|
+
anchor: ReactNode | React.RefObject<Element>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The content to display inside the popover
|
|
28
|
+
*/
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Preferred placement of the popover relative to anchor
|
|
33
|
+
*/
|
|
34
|
+
placement?: PopoverPlacement;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Distance from the anchor element in pixels
|
|
38
|
+
*/
|
|
39
|
+
offset?: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether clicking outside should close the popover
|
|
43
|
+
*/
|
|
44
|
+
closeOnClickOutside?: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether pressing escape key should close the popover (web only)
|
|
48
|
+
*/
|
|
49
|
+
closeOnEscapeKey?: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether to show an arrow pointing to the anchor
|
|
53
|
+
*/
|
|
54
|
+
showArrow?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Additional styles (platform-specific)
|
|
58
|
+
*/
|
|
59
|
+
style?: any;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Test ID for testing
|
|
63
|
+
*/
|
|
64
|
+
testID?: string;
|
|
65
|
+
}
|
|
@@ -11,6 +11,8 @@ import { DividerExamples } from './DividerExamples';
|
|
|
11
11
|
import { BadgeExamples } from './BadgeExamples';
|
|
12
12
|
import { AvatarExamples } from './AvatarExamples';
|
|
13
13
|
import { ScreenExamples } from './ScreenExamples';
|
|
14
|
+
import { DialogExamples } from './DialogExamples';
|
|
15
|
+
import { PopoverExamples } from './PopoverExamples';
|
|
14
16
|
import { ThemeExtensionExamples } from './ThemeExtensionExamples';
|
|
15
17
|
|
|
16
18
|
export const AllExamples = () => {
|
|
@@ -60,6 +62,12 @@ export const AllExamples = () => {
|
|
|
60
62
|
|
|
61
63
|
<ScreenExamples />
|
|
62
64
|
<Divider spacing="medium" />
|
|
65
|
+
|
|
66
|
+
<DialogExamples />
|
|
67
|
+
<Divider spacing="medium" />
|
|
68
|
+
|
|
69
|
+
<PopoverExamples />
|
|
70
|
+
<Divider spacing="medium" />
|
|
63
71
|
|
|
64
72
|
<Divider spacing="large" intent="success">
|
|
65
73
|
<Text size="small" weight="semibold" color="green">THEME SYSTEM</Text>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Screen, View, Button, Text, Dialog } from '../index';
|
|
3
|
+
|
|
4
|
+
export const DialogExamples = () => {
|
|
5
|
+
const [basicOpen, setBasicOpen] = useState(false);
|
|
6
|
+
const [alertOpen, setAlertOpen] = useState(false);
|
|
7
|
+
const [confirmationOpen, setConfirmationOpen] = useState(false);
|
|
8
|
+
const [sizesOpen, setSizesOpen] = useState<string | null>(null);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Screen background="primary" padding="lg">
|
|
12
|
+
<View spacing="none">
|
|
13
|
+
<Text size="large" weight="bold" align="center">
|
|
14
|
+
Dialog Examples
|
|
15
|
+
</Text>
|
|
16
|
+
|
|
17
|
+
{/* Basic Dialog */}
|
|
18
|
+
<View spacing="md">
|
|
19
|
+
<Text size="medium" weight="semibold">Basic Dialog</Text>
|
|
20
|
+
<Button onPress={() => setBasicOpen(true)}>
|
|
21
|
+
Open Basic Dialog
|
|
22
|
+
</Button>
|
|
23
|
+
<Dialog
|
|
24
|
+
open={basicOpen}
|
|
25
|
+
onOpenChange={setBasicOpen}
|
|
26
|
+
title="Basic Dialog"
|
|
27
|
+
>
|
|
28
|
+
<Text>This is a basic dialog with a title and some content.</Text>
|
|
29
|
+
<View spacing="md" style={{ marginTop: 16 }}>
|
|
30
|
+
<Button
|
|
31
|
+
variant="contained"
|
|
32
|
+
intent="primary"
|
|
33
|
+
onPress={() => setBasicOpen(false)}
|
|
34
|
+
>
|
|
35
|
+
Close Dialog
|
|
36
|
+
</Button>
|
|
37
|
+
</View>
|
|
38
|
+
</Dialog>
|
|
39
|
+
</View>
|
|
40
|
+
|
|
41
|
+
{/* Dialog Variants */}
|
|
42
|
+
<View spacing="md">
|
|
43
|
+
<Text size="medium" weight="semibold">Dialog Variants</Text>
|
|
44
|
+
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
|
|
45
|
+
<Button onPress={() => setAlertOpen(true)}>
|
|
46
|
+
Alert Dialog
|
|
47
|
+
</Button>
|
|
48
|
+
<Button onPress={() => setConfirmationOpen(true)}>
|
|
49
|
+
Confirmation Dialog
|
|
50
|
+
</Button>
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
{/* Alert Dialog */}
|
|
54
|
+
<Dialog
|
|
55
|
+
open={alertOpen}
|
|
56
|
+
onOpenChange={setAlertOpen}
|
|
57
|
+
title="Important Alert"
|
|
58
|
+
variant="alert"
|
|
59
|
+
>
|
|
60
|
+
<Text>This is an alert dialog. It has a top border to indicate importance.</Text>
|
|
61
|
+
<View spacing="md" style={{ marginTop: 16 }}>
|
|
62
|
+
<Button
|
|
63
|
+
variant="contained"
|
|
64
|
+
intent="primary"
|
|
65
|
+
onPress={() => setAlertOpen(false)}
|
|
66
|
+
>
|
|
67
|
+
Acknowledge
|
|
68
|
+
</Button>
|
|
69
|
+
</View>
|
|
70
|
+
</Dialog>
|
|
71
|
+
|
|
72
|
+
{/* Confirmation Dialog */}
|
|
73
|
+
<Dialog
|
|
74
|
+
open={confirmationOpen}
|
|
75
|
+
onOpenChange={setConfirmationOpen}
|
|
76
|
+
title="Confirm Action"
|
|
77
|
+
variant="confirmation"
|
|
78
|
+
closeOnBackdropClick={false}
|
|
79
|
+
>
|
|
80
|
+
<Text>Are you sure you want to delete this item? This action cannot be undone.</Text>
|
|
81
|
+
<View style={{ flexDirection: 'row', gap: 12, marginTop: 16 }}>
|
|
82
|
+
<Button
|
|
83
|
+
variant="outlined"
|
|
84
|
+
intent="neutral"
|
|
85
|
+
onPress={() => setConfirmationOpen(false)}
|
|
86
|
+
>
|
|
87
|
+
Cancel
|
|
88
|
+
</Button>
|
|
89
|
+
<Button
|
|
90
|
+
variant="contained"
|
|
91
|
+
intent="error"
|
|
92
|
+
onPress={() => setConfirmationOpen(false)}
|
|
93
|
+
>
|
|
94
|
+
Delete
|
|
95
|
+
</Button>
|
|
96
|
+
</View>
|
|
97
|
+
</Dialog>
|
|
98
|
+
</View>
|
|
99
|
+
|
|
100
|
+
{/* Dialog Sizes */}
|
|
101
|
+
<View spacing="md">
|
|
102
|
+
<Text size="medium" weight="semibold">Dialog Sizes</Text>
|
|
103
|
+
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
|
|
104
|
+
{['small', 'medium', 'large'].map((size) => (
|
|
105
|
+
<Button
|
|
106
|
+
key={size}
|
|
107
|
+
onPress={() => setSizesOpen(size)}
|
|
108
|
+
>
|
|
109
|
+
{size.charAt(0).toUpperCase() + size.slice(1)} Dialog
|
|
110
|
+
</Button>
|
|
111
|
+
))}
|
|
112
|
+
</View>
|
|
113
|
+
|
|
114
|
+
{sizesOpen && (
|
|
115
|
+
<Dialog
|
|
116
|
+
open={!!sizesOpen}
|
|
117
|
+
onOpenChange={() => setSizesOpen(null)}
|
|
118
|
+
title={`${sizesOpen.charAt(0).toUpperCase() + sizesOpen.slice(1)} Dialog`}
|
|
119
|
+
size={sizesOpen as 'small' | 'medium' | 'large'}
|
|
120
|
+
>
|
|
121
|
+
<Text>
|
|
122
|
+
This is a {sizesOpen} dialog. The width and maximum width are adjusted based on the size prop.
|
|
123
|
+
</Text>
|
|
124
|
+
<View spacing="md" style={{ marginTop: 16 }}>
|
|
125
|
+
<Button
|
|
126
|
+
variant="contained"
|
|
127
|
+
intent="primary"
|
|
128
|
+
onPress={() => setSizesOpen(null)}
|
|
129
|
+
>
|
|
130
|
+
Close
|
|
131
|
+
</Button>
|
|
132
|
+
</View>
|
|
133
|
+
</Dialog>
|
|
134
|
+
)}
|
|
135
|
+
</View>
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
{/* Dialog Options */}
|
|
139
|
+
<View spacing="md">
|
|
140
|
+
<Text size="medium" weight="semibold">Dialog Options</Text>
|
|
141
|
+
<Text size="small" color="secondary">
|
|
142
|
+
• Close on backdrop click: Enabled by default, disabled for confirmation dialog above
|
|
143
|
+
</Text>
|
|
144
|
+
<Text size="small" color="secondary">
|
|
145
|
+
• Close on escape key: Enabled by default (web only)
|
|
146
|
+
</Text>
|
|
147
|
+
<Text size="small" color="secondary">
|
|
148
|
+
• Hardware back button: Handled automatically (native only)
|
|
149
|
+
</Text>
|
|
150
|
+
<Text size="small" color="secondary">
|
|
151
|
+
• Focus management: Automatic focus trapping and restoration (web only)
|
|
152
|
+
</Text>
|
|
153
|
+
</View>
|
|
154
|
+
</View>
|
|
155
|
+
</Screen>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
import { Screen, View, Button, Text, Popover } from '../index';
|
|
3
|
+
|
|
4
|
+
export const PopoverExamples = () => {
|
|
5
|
+
const [basicOpen, setBasicOpen] = useState(false);
|
|
6
|
+
const [placementOpen, setPlacementOpen] = useState<string | null>(null);
|
|
7
|
+
const [arrowOpen, setArrowOpen] = useState(false);
|
|
8
|
+
|
|
9
|
+
const basicButtonRef = useRef<HTMLDivElement>(null);
|
|
10
|
+
const placementButtonRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
|
11
|
+
const arrowButtonRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
|
|
13
|
+
const placements = [
|
|
14
|
+
'top', 'top-start', 'top-end',
|
|
15
|
+
'bottom', 'bottom-start', 'bottom-end',
|
|
16
|
+
'left', 'left-start', 'left-end',
|
|
17
|
+
'right', 'right-start', 'right-end',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Screen background="primary" padding="lg">
|
|
22
|
+
<View spacing="none">
|
|
23
|
+
<Text size="large" weight="bold" align="center">
|
|
24
|
+
Popover Examples
|
|
25
|
+
</Text>
|
|
26
|
+
|
|
27
|
+
{/* Basic Popover */}
|
|
28
|
+
<View spacing="md">
|
|
29
|
+
<Text size="medium" weight="semibold">Basic Popover</Text>
|
|
30
|
+
<div ref={basicButtonRef} style={{ display: 'inline-block' }}>
|
|
31
|
+
<Button onPress={() => setBasicOpen(true)}>
|
|
32
|
+
Open Basic Popover
|
|
33
|
+
</Button>
|
|
34
|
+
</div>
|
|
35
|
+
<Popover
|
|
36
|
+
open={basicOpen}
|
|
37
|
+
onOpenChange={setBasicOpen}
|
|
38
|
+
anchor={basicButtonRef}
|
|
39
|
+
placement="bottom"
|
|
40
|
+
>
|
|
41
|
+
<View spacing="sm">
|
|
42
|
+
<Text weight="bold">Basic Popover</Text>
|
|
43
|
+
<Text size="small">This is a basic popover with some content.</Text>
|
|
44
|
+
<Button size="small" onPress={() => setBasicOpen(false)}>
|
|
45
|
+
Close
|
|
46
|
+
</Button>
|
|
47
|
+
</View>
|
|
48
|
+
</Popover>
|
|
49
|
+
</View>
|
|
50
|
+
|
|
51
|
+
{/* Placement Examples */}
|
|
52
|
+
<View spacing="md">
|
|
53
|
+
<Text size="medium" weight="semibold">Placement Options</Text>
|
|
54
|
+
<View style={{
|
|
55
|
+
display: 'grid',
|
|
56
|
+
gridTemplateColumns: 'repeat(3, 1fr)',
|
|
57
|
+
gap: 8,
|
|
58
|
+
maxWidth: 400
|
|
59
|
+
}}>
|
|
60
|
+
{placements.map((placement) => (
|
|
61
|
+
<div
|
|
62
|
+
key={placement}
|
|
63
|
+
ref={(ref) => placementButtonRefs.current[placement] = ref}
|
|
64
|
+
style={{ display: 'inline-block' }}
|
|
65
|
+
>
|
|
66
|
+
<Button
|
|
67
|
+
size="small"
|
|
68
|
+
variant="outlined"
|
|
69
|
+
onPress={() => setPlacementOpen(placement)}
|
|
70
|
+
>
|
|
71
|
+
{placement}
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</View>
|
|
76
|
+
|
|
77
|
+
{placementOpen && (
|
|
78
|
+
<Popover
|
|
79
|
+
open={!!placementOpen}
|
|
80
|
+
onOpenChange={() => setPlacementOpen(null)}
|
|
81
|
+
anchor={{ current: placementButtonRefs.current[placementOpen] }}
|
|
82
|
+
placement={placementOpen as any}
|
|
83
|
+
>
|
|
84
|
+
<View spacing="sm">
|
|
85
|
+
<Text weight="bold">{placementOpen} placement</Text>
|
|
86
|
+
<Text size="small">
|
|
87
|
+
Positioned {placementOpen} relative to the button
|
|
88
|
+
</Text>
|
|
89
|
+
<Button size="small" onPress={() => setPlacementOpen(null)}>
|
|
90
|
+
Close
|
|
91
|
+
</Button>
|
|
92
|
+
</View>
|
|
93
|
+
</Popover>
|
|
94
|
+
)}
|
|
95
|
+
</View>
|
|
96
|
+
|
|
97
|
+
{/* Arrow Example */}
|
|
98
|
+
<View spacing="md">
|
|
99
|
+
<Text size="medium" weight="semibold">With Arrow</Text>
|
|
100
|
+
<div ref={arrowButtonRef} style={{ display: 'inline-block' }}>
|
|
101
|
+
<Button
|
|
102
|
+
variant="contained"
|
|
103
|
+
intent="success"
|
|
104
|
+
onPress={() => setArrowOpen(true)}
|
|
105
|
+
>
|
|
106
|
+
Popover with Arrow
|
|
107
|
+
</Button>
|
|
108
|
+
</div>
|
|
109
|
+
<Popover
|
|
110
|
+
open={arrowOpen}
|
|
111
|
+
onOpenChange={setArrowOpen}
|
|
112
|
+
anchor={arrowButtonRef}
|
|
113
|
+
placement="top"
|
|
114
|
+
showArrow={true}
|
|
115
|
+
>
|
|
116
|
+
<View spacing="sm">
|
|
117
|
+
<Text weight="bold">Arrow Popover</Text>
|
|
118
|
+
<Text size="small">
|
|
119
|
+
This popover includes an arrow pointing to the anchor element.
|
|
120
|
+
</Text>
|
|
121
|
+
<Button size="small" onPress={() => setArrowOpen(false)}>
|
|
122
|
+
Close
|
|
123
|
+
</Button>
|
|
124
|
+
</View>
|
|
125
|
+
</Popover>
|
|
126
|
+
</View>
|
|
127
|
+
|
|
128
|
+
{/* Features Description */}
|
|
129
|
+
<View spacing="md">
|
|
130
|
+
<Text size="medium" weight="semibold">Features</Text>
|
|
131
|
+
<View spacing="sm">
|
|
132
|
+
<Text size="small" color="secondary">
|
|
133
|
+
• Automatically positions within viewport bounds
|
|
134
|
+
</Text>
|
|
135
|
+
<Text size="small" color="secondary">
|
|
136
|
+
• 12 placement options (top, bottom, left, right with start/end variants)
|
|
137
|
+
</Text>
|
|
138
|
+
<Text size="small" color="secondary">
|
|
139
|
+
• Optional arrow pointing to anchor element
|
|
140
|
+
</Text>
|
|
141
|
+
<Text size="small" color="secondary">
|
|
142
|
+
• Click outside or escape key to close
|
|
143
|
+
</Text>
|
|
144
|
+
<Text size="small" color="secondary">
|
|
145
|
+
• Smooth animations and transitions
|
|
146
|
+
</Text>
|
|
147
|
+
<Text size="small" color="secondary">
|
|
148
|
+
• Follows anchor element on scroll/resize (web)
|
|
149
|
+
</Text>
|
|
150
|
+
</View>
|
|
151
|
+
</View>
|
|
152
|
+
</View>
|
|
153
|
+
</Screen>
|
|
154
|
+
);
|
|
155
|
+
};
|