@ecohouse/ui 0.1.31 → 0.1.33
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 +5 -0
- package/dist/index.cjs +1594 -523
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -10
- package/dist/index.d.ts +218 -10
- package/dist/index.js +1516 -471
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AccountHeader/AccountHeader.stories.tsx +70 -0
- package/src/components/AccountHeader/AccountHeader.tsx +224 -0
- package/src/components/AccountHeader/index.ts +2 -0
- package/src/components/AvatarSimple/AvatarSimple.tsx +2 -2
- package/src/components/Button/Button.tsx +4 -1
- package/src/components/DatePicker/DatePicker.tsx +129 -54
- package/src/components/FloatingActionButton/FloatingActionButton.stories.tsx +42 -0
- package/src/components/FloatingActionButton/FloatingActionButton.tsx +77 -0
- package/src/components/FloatingActionButton/index.ts +2 -0
- package/src/components/InfoTile/InfoTile.stories.tsx +80 -0
- package/src/components/InfoTile/InfoTile.tsx +118 -0
- package/src/components/InfoTile/index.ts +2 -0
- package/src/components/PaginationDots/PaginationDots.stories.tsx +48 -0
- package/src/components/PaginationDots/PaginationDots.tsx +111 -0
- package/src/components/PaginationDots/index.ts +2 -0
- package/src/components/Select/Select.tsx +5 -2
- package/src/components/Sidebar/Sidebar.tsx +25 -11
- package/src/components/StatusBadge/StatusBadge.stories.tsx +54 -0
- package/src/components/StatusBadge/StatusBadge.tsx +108 -0
- package/src/components/StatusBadge/index.ts +2 -0
- package/src/components/StepList/StepList.stories.tsx +37 -0
- package/src/components/StepList/StepList.tsx +152 -0
- package/src/components/StepList/index.ts +2 -0
- package/src/components/StepPager/StepPager.tsx +150 -0
- package/src/components/StepPager/index.ts +2 -0
- package/src/components/index.ts +21 -0
- package/src/icons/AlertTriangle/AlertTriangleIcon.tsx +19 -0
- package/src/icons/AlertTriangle/AlertTriangleIcon.web.tsx +18 -0
- package/src/icons/AlertTriangle/alertTrianglePath.ts +3 -0
- package/src/icons/AlertTriangle/index.ts +1 -0
- package/src/icons/Ban/BanIcon.tsx +15 -0
- package/src/icons/Ban/banPath.ts +2 -0
- package/src/icons/Ban/index.ts +1 -0
- package/src/icons/Bell/BellIcon.tsx +2 -2
- package/src/icons/Bell/BellIcon.web.tsx +2 -2
- package/src/icons/Bell/bellPath.ts +1 -1
- package/src/icons/CottageDetail/CottageDetailIcons.tsx +15 -0
- package/src/icons/CottageDetail/CottageDetailIcons.web.tsx +15 -0
- package/src/icons/CottageDetail/cottageDetailPaths.ts +3 -0
- package/src/icons/CottageDetail/index.ts +1 -0
- package/src/icons/Edit/EditIcon.tsx +20 -0
- package/src/icons/Edit/EditIcon.web.tsx +19 -0
- package/src/icons/Edit/editPath.ts +3 -0
- package/src/icons/Edit/index.ts +1 -0
- package/src/icons/Fullscreen/FullscreenIcon.tsx +21 -0
- package/src/icons/Fullscreen/fullscreenPath.ts +2 -0
- package/src/icons/Fullscreen/index.ts +1 -0
- package/src/icons/Home/homePath.ts +1 -1
- package/src/icons/HouseRules/HouseRulesIcons.tsx +62 -0
- package/src/icons/HouseRules/HouseRulesIcons.web.tsx +61 -0
- package/src/icons/HouseRules/houseRulesPaths.ts +24 -0
- package/src/icons/HouseRules/index.ts +2 -0
- package/src/icons/LocationUser/LocationUserIcon.tsx +15 -0
- package/src/icons/LocationUser/LocationUserIcon.web.tsx +14 -0
- package/src/icons/LocationUser/index.ts +1 -0
- package/src/icons/LocationUser/locationUserPath.ts +2 -0
- package/src/icons/Lock/LockIcon.tsx +21 -0
- package/src/icons/Lock/LockIcon.web.tsx +26 -0
- package/src/icons/Lock/index.ts +1 -0
- package/src/icons/Lock/lockPath.ts +3 -0
- package/src/icons/Refresh/RefreshIcon.tsx +20 -0
- package/src/icons/Refresh/RefreshIcon.web.tsx +19 -0
- package/src/icons/Refresh/index.ts +1 -0
- package/src/icons/Refresh/refreshPath.ts +3 -0
- package/src/icons/RefundStatus/RefundStatusIcon.tsx +24 -0
- package/src/icons/RefundStatus/RefundStatusIcon.web.tsx +23 -0
- package/src/icons/RefundStatus/index.ts +1 -0
- package/src/icons/RefundStatus/refundStatusPath.ts +3 -0
- package/src/icons/Sort/SortIcon.tsx +3 -3
- package/src/icons/Sort/SortIcon.web.tsx +2 -2
- package/src/icons/Sort/sortPath.ts +3 -2
- package/src/icons/VideoOff/VideoOffIcon.tsx +21 -0
- package/src/icons/VideoOff/VideoOffIcon.web.tsx +27 -0
- package/src/icons/VideoOff/index.ts +1 -0
- package/src/icons/VideoOff/videoOffPath.ts +2 -0
- package/src/icons/ZoomOut/ZoomOutIcon.tsx +21 -0
- package/src/icons/ZoomOut/index.ts +1 -0
- package/src/icons/ZoomOut/zoomOutPath.ts +2 -0
- package/src/icons/index.ts +12 -0
- package/src/icons/registry.ts +42 -0
- package/src/index.ts +34 -0
- package/src/theme/colors.test.ts +1 -1
- package/src/theme/colors.ts +4 -0
- package/src/theme/typography.ts +5 -5
- package/src/utils/dateUtils.ts +25 -0
- package/src/utils/popoverAlign.ts +85 -0
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { fn } from "storybook/test";
|
|
3
|
+
import { StyleSheet, View } from "react-native";
|
|
4
|
+
import { StatusBadge } from "../StatusBadge";
|
|
5
|
+
import { AccountHeader } from "./AccountHeader";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Components/AccountHeader",
|
|
9
|
+
component: AccountHeader,
|
|
10
|
+
args: {
|
|
11
|
+
title: "Իմ ամրագրումները",
|
|
12
|
+
onNotificationsPress: fn(),
|
|
13
|
+
notificationsAccessibilityLabel: "Ծանուցումներ",
|
|
14
|
+
menuAccessibilityLabel: "Բացել ընտրացանկը",
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
onMenuPress: { action: "menu press" },
|
|
18
|
+
onNotificationsPress: { action: "notifications press" },
|
|
19
|
+
},
|
|
20
|
+
parameters: {
|
|
21
|
+
backgrounds: { default: "black" },
|
|
22
|
+
layout: "fullscreen",
|
|
23
|
+
},
|
|
24
|
+
} satisfies Meta<typeof AccountHeader>;
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
export const Desktop: Story = {
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<View style={styles.desktopFrame}>
|
|
32
|
+
<AccountHeader {...args} />
|
|
33
|
+
</View>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Mobile: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
onMenuPress: fn(),
|
|
40
|
+
},
|
|
41
|
+
render: (args) => (
|
|
42
|
+
<View style={styles.mobileFrame}>
|
|
43
|
+
<AccountHeader {...args} />
|
|
44
|
+
</View>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const WithBackAndStatus: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
title: "Ամրագրում #BK-9087",
|
|
51
|
+
onBackPress: fn(),
|
|
52
|
+
},
|
|
53
|
+
render: (args) => (
|
|
54
|
+
<View style={styles.desktopFrame}>
|
|
55
|
+
<AccountHeader {...args} badge={<StatusBadge label="Ակտիվ" tone="active" />} />
|
|
56
|
+
</View>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const styles = StyleSheet.create({
|
|
61
|
+
desktopFrame: {
|
|
62
|
+
width: "100%",
|
|
63
|
+
maxWidth: 960,
|
|
64
|
+
backgroundColor: "#090909",
|
|
65
|
+
},
|
|
66
|
+
mobileFrame: {
|
|
67
|
+
width: 390,
|
|
68
|
+
backgroundColor: "#090909",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { forwardRef, useMemo, useRef, type ComponentRef, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
View,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
type ViewStyle,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { BellIcon, ChevronLeftIcon, MenuIcon } from "../../icons";
|
|
12
|
+
import { colors, fonts } from "../../theme";
|
|
13
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
14
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
15
|
+
|
|
16
|
+
export interface AccountHeaderProps extends Omit<ViewProps, "style" | "children"> {
|
|
17
|
+
title: string;
|
|
18
|
+
/**
|
|
19
|
+
* When set, shows a 32×32 back control (takes priority over `onMenuPress`).
|
|
20
|
+
* Used on nested account pages such as booking details.
|
|
21
|
+
*/
|
|
22
|
+
onBackPress?: () => void;
|
|
23
|
+
/** When set, shows the 28×28 menu control (mobile account shell). Ignored if `onBackPress` is set. */
|
|
24
|
+
onMenuPress?: () => void;
|
|
25
|
+
onNotificationsPress?: () => void;
|
|
26
|
+
/** Optional status chip (or other accessory) rendered after the title with a 12px gap. */
|
|
27
|
+
badge?: ReactNode;
|
|
28
|
+
/** Accessible name for the back control. Defaults to `"Back"`. */
|
|
29
|
+
backAccessibilityLabel?: string;
|
|
30
|
+
/** Accessible name for the menu control. Defaults to `"Open menu"`. */
|
|
31
|
+
menuAccessibilityLabel?: string;
|
|
32
|
+
/** Accessible name for notifications. Defaults to `"Notifications"`. */
|
|
33
|
+
notificationsAccessibilityLabel?: string;
|
|
34
|
+
style?: StyleProp<ViewStyle>;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const HEADER_HEIGHT = 64;
|
|
39
|
+
const MENU_ICON_SIZE = 28;
|
|
40
|
+
const ICON_BUTTON_SIZE = 32;
|
|
41
|
+
const ICON_BUTTON_ICON_SIZE = 16;
|
|
42
|
+
const BACK_TITLE_GAP = 12;
|
|
43
|
+
const TITLE_BADGE_GAP = 12;
|
|
44
|
+
const MENU_TITLE_GAP = 16;
|
|
45
|
+
const RIGHT_GAP = 16;
|
|
46
|
+
|
|
47
|
+
export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeaderProps>(
|
|
48
|
+
function AccountHeader(
|
|
49
|
+
{
|
|
50
|
+
title,
|
|
51
|
+
onBackPress,
|
|
52
|
+
onMenuPress,
|
|
53
|
+
onNotificationsPress,
|
|
54
|
+
badge,
|
|
55
|
+
backAccessibilityLabel = "Back",
|
|
56
|
+
menuAccessibilityLabel = "Open menu",
|
|
57
|
+
notificationsAccessibilityLabel = "Notifications",
|
|
58
|
+
style,
|
|
59
|
+
className,
|
|
60
|
+
...rest
|
|
61
|
+
},
|
|
62
|
+
forwardedRef,
|
|
63
|
+
) {
|
|
64
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
65
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
66
|
+
const resolvedClassName = className?.trim() || undefined;
|
|
67
|
+
const showBack = onBackPress != null;
|
|
68
|
+
const showMenu = !showBack && onMenuPress != null;
|
|
69
|
+
const isCompact = showMenu;
|
|
70
|
+
|
|
71
|
+
useApplyWebClassName(containerRef, resolvedClassName);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<View
|
|
75
|
+
{...rest}
|
|
76
|
+
ref={setContainerRef}
|
|
77
|
+
accessibilityRole="header"
|
|
78
|
+
style={[styles.base, isCompact ? styles.compact : styles.desktop, style]}
|
|
79
|
+
>
|
|
80
|
+
<View
|
|
81
|
+
style={[
|
|
82
|
+
styles.start,
|
|
83
|
+
showBack ? styles.startWithBack : null,
|
|
84
|
+
showMenu ? styles.startWithMenu : null,
|
|
85
|
+
]}
|
|
86
|
+
>
|
|
87
|
+
{showBack ? (
|
|
88
|
+
<Pressable
|
|
89
|
+
accessibilityRole="button"
|
|
90
|
+
accessibilityLabel={backAccessibilityLabel}
|
|
91
|
+
hitSlop={8}
|
|
92
|
+
onPress={onBackPress}
|
|
93
|
+
style={styles.iconButton}
|
|
94
|
+
>
|
|
95
|
+
<ChevronLeftIcon
|
|
96
|
+
color={colors.white}
|
|
97
|
+
size={ICON_BUTTON_ICON_SIZE}
|
|
98
|
+
strokeWidth={1.5}
|
|
99
|
+
/>
|
|
100
|
+
</Pressable>
|
|
101
|
+
) : null}
|
|
102
|
+
|
|
103
|
+
{showMenu ? (
|
|
104
|
+
<Pressable
|
|
105
|
+
accessibilityRole="button"
|
|
106
|
+
accessibilityLabel={menuAccessibilityLabel}
|
|
107
|
+
hitSlop={8}
|
|
108
|
+
onPress={onMenuPress}
|
|
109
|
+
style={styles.menuButton}
|
|
110
|
+
>
|
|
111
|
+
<MenuIcon color={colors.white} size={MENU_ICON_SIZE} strokeWidth={1.5} />
|
|
112
|
+
</Pressable>
|
|
113
|
+
) : null}
|
|
114
|
+
|
|
115
|
+
<View style={styles.titleRow}>
|
|
116
|
+
<Text
|
|
117
|
+
numberOfLines={1}
|
|
118
|
+
style={[
|
|
119
|
+
styles.title,
|
|
120
|
+
showBack || !isCompact ? styles.titleDesktop : styles.titleCompact,
|
|
121
|
+
]}
|
|
122
|
+
>
|
|
123
|
+
{title}
|
|
124
|
+
</Text>
|
|
125
|
+
{badge != null ? <View style={styles.badgeSlot}>{badge}</View> : null}
|
|
126
|
+
</View>
|
|
127
|
+
</View>
|
|
128
|
+
|
|
129
|
+
<View style={styles.end}>
|
|
130
|
+
<Pressable
|
|
131
|
+
accessibilityRole="button"
|
|
132
|
+
accessibilityLabel={notificationsAccessibilityLabel}
|
|
133
|
+
onPress={onNotificationsPress}
|
|
134
|
+
style={styles.iconButton}
|
|
135
|
+
>
|
|
136
|
+
<BellIcon color={colors.white} size={ICON_BUTTON_ICON_SIZE} strokeWidth={1.5} />
|
|
137
|
+
</Pressable>
|
|
138
|
+
</View>
|
|
139
|
+
</View>
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const styles = StyleSheet.create({
|
|
145
|
+
base: {
|
|
146
|
+
height: HEADER_HEIGHT,
|
|
147
|
+
width: "100%",
|
|
148
|
+
flexDirection: "row",
|
|
149
|
+
alignItems: "center",
|
|
150
|
+
justifyContent: "space-between",
|
|
151
|
+
backgroundColor: colors.dark,
|
|
152
|
+
borderBottomWidth: 1,
|
|
153
|
+
borderBottomColor: colors.grey700,
|
|
154
|
+
},
|
|
155
|
+
desktop: {
|
|
156
|
+
paddingTop: 16,
|
|
157
|
+
paddingBottom: 16,
|
|
158
|
+
paddingLeft: 24,
|
|
159
|
+
paddingRight: 24,
|
|
160
|
+
},
|
|
161
|
+
compact: {
|
|
162
|
+
padding: 16,
|
|
163
|
+
},
|
|
164
|
+
start: {
|
|
165
|
+
flex: 1,
|
|
166
|
+
minWidth: 0,
|
|
167
|
+
flexDirection: "row",
|
|
168
|
+
alignItems: "center",
|
|
169
|
+
},
|
|
170
|
+
startWithBack: {
|
|
171
|
+
gap: BACK_TITLE_GAP,
|
|
172
|
+
},
|
|
173
|
+
startWithMenu: {
|
|
174
|
+
gap: MENU_TITLE_GAP,
|
|
175
|
+
},
|
|
176
|
+
titleRow: {
|
|
177
|
+
flex: 1,
|
|
178
|
+
minWidth: 0,
|
|
179
|
+
flexDirection: "row",
|
|
180
|
+
alignItems: "center",
|
|
181
|
+
gap: TITLE_BADGE_GAP,
|
|
182
|
+
},
|
|
183
|
+
end: {
|
|
184
|
+
flexShrink: 0,
|
|
185
|
+
marginLeft: RIGHT_GAP,
|
|
186
|
+
},
|
|
187
|
+
iconButton: {
|
|
188
|
+
width: ICON_BUTTON_SIZE,
|
|
189
|
+
height: ICON_BUTTON_SIZE,
|
|
190
|
+
padding: 8,
|
|
191
|
+
alignItems: "center",
|
|
192
|
+
justifyContent: "center",
|
|
193
|
+
borderRadius: 5,
|
|
194
|
+
backgroundColor: colors.grey800,
|
|
195
|
+
borderWidth: 1,
|
|
196
|
+
borderColor: colors.grey700,
|
|
197
|
+
flexShrink: 0,
|
|
198
|
+
},
|
|
199
|
+
menuButton: {
|
|
200
|
+
width: MENU_ICON_SIZE,
|
|
201
|
+
height: MENU_ICON_SIZE,
|
|
202
|
+
alignItems: "center",
|
|
203
|
+
justifyContent: "center",
|
|
204
|
+
flexShrink: 0,
|
|
205
|
+
},
|
|
206
|
+
title: {
|
|
207
|
+
flexShrink: 1,
|
|
208
|
+
fontFamily: fonts.sans,
|
|
209
|
+
fontWeight: "600",
|
|
210
|
+
letterSpacing: 0,
|
|
211
|
+
color: colors.white,
|
|
212
|
+
},
|
|
213
|
+
titleDesktop: {
|
|
214
|
+
fontSize: 16,
|
|
215
|
+
lineHeight: 22,
|
|
216
|
+
},
|
|
217
|
+
titleCompact: {
|
|
218
|
+
fontSize: 14,
|
|
219
|
+
lineHeight: 20,
|
|
220
|
+
},
|
|
221
|
+
badgeSlot: {
|
|
222
|
+
flexShrink: 0,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
@@ -83,10 +83,10 @@ export const AvatarSimple = forwardRef<ComponentRef<typeof View>, AvatarSimplePr
|
|
|
83
83
|
{!compact ? (
|
|
84
84
|
<View style={styles.details}>
|
|
85
85
|
<View style={styles.textBlock}>
|
|
86
|
-
<Text style={styles.name} numberOfLines={1}>
|
|
86
|
+
<Text style={styles.name} numberOfLines={1} ellipsizeMode="tail">
|
|
87
87
|
{name}
|
|
88
88
|
</Text>
|
|
89
|
-
<Text style={styles.email} numberOfLines={1}>
|
|
89
|
+
<Text style={styles.email} numberOfLines={1} ellipsizeMode="tail">
|
|
90
90
|
{email}
|
|
91
91
|
</Text>
|
|
92
92
|
</View>
|
|
@@ -31,6 +31,8 @@ export interface ButtonProps extends Omit<
|
|
|
31
31
|
size?: ButtonSize;
|
|
32
32
|
showIcon?: boolean;
|
|
33
33
|
icon?: ReactNode;
|
|
34
|
+
/** Overrides the trailing icon badge fill (defaults to the variant preset). */
|
|
35
|
+
iconBadgeBackgroundColor?: string;
|
|
34
36
|
style?: StyleProp<ViewStyle>;
|
|
35
37
|
textStyle?: StyleProp<TextStyle>;
|
|
36
38
|
className?: string;
|
|
@@ -97,6 +99,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
|
|
|
97
99
|
size = "lg",
|
|
98
100
|
showIcon = false,
|
|
99
101
|
icon,
|
|
102
|
+
iconBadgeBackgroundColor,
|
|
100
103
|
style,
|
|
101
104
|
textStyle,
|
|
102
105
|
className,
|
|
@@ -182,7 +185,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
|
|
|
182
185
|
width: metrics.iconContainerSize,
|
|
183
186
|
height: metrics.iconContainerSize,
|
|
184
187
|
borderRadius: metrics.iconContainerSize / 2,
|
|
185
|
-
backgroundColor: preset.iconBadgeBackgroundColor,
|
|
188
|
+
backgroundColor: iconBadgeBackgroundColor ?? preset.iconBadgeBackgroundColor,
|
|
186
189
|
},
|
|
187
190
|
]}
|
|
188
191
|
>
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
forwardRef,
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
@@ -22,6 +23,7 @@ import { colors, datePickerTypography } from "../../theme";
|
|
|
22
23
|
import {
|
|
23
24
|
addMonths,
|
|
24
25
|
formatDate,
|
|
26
|
+
formatDateDayShortMonth,
|
|
25
27
|
getMonthMatrix,
|
|
26
28
|
isBefore,
|
|
27
29
|
isDateDisabled,
|
|
@@ -92,6 +94,11 @@ type DatePickerBaseProps = Omit<ViewProps, "style" | "children"> & {
|
|
|
92
94
|
weekStartsOn?: 0 | 1;
|
|
93
95
|
/** Styles the calendar popup independently from the trigger width. */
|
|
94
96
|
calendarStyle?: StyleProp<ViewStyle>;
|
|
97
|
+
/**
|
|
98
|
+
* When true, only the calendar panel is rendered (no field/trigger).
|
|
99
|
+
* Use with controlled `open` for icon-button / popover hosts.
|
|
100
|
+
*/
|
|
101
|
+
hideTrigger?: boolean;
|
|
95
102
|
style?: StyleProp<ViewStyle>;
|
|
96
103
|
className?: string;
|
|
97
104
|
};
|
|
@@ -137,9 +144,7 @@ function RangeTriggerField({
|
|
|
137
144
|
{label}
|
|
138
145
|
</Text>
|
|
139
146
|
<View style={styles.rangeValueRow}>
|
|
140
|
-
<Text
|
|
141
|
-
{value}
|
|
142
|
-
</Text>
|
|
147
|
+
<Text style={styles.rangeDate}>{value}</Text>
|
|
143
148
|
{time ? (
|
|
144
149
|
<View style={styles.rangeTimeWrap}>
|
|
145
150
|
<ClockFilledIcon color={colors.lightGrey} size={16} />
|
|
@@ -172,9 +177,10 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
172
177
|
defaultVisibleMonth,
|
|
173
178
|
monthNames = DEFAULT_MONTH_NAMES,
|
|
174
179
|
weekdayLabels = DEFAULT_WEEKDAY_LABELS,
|
|
175
|
-
formatValue
|
|
180
|
+
formatValue: formatValueProp,
|
|
176
181
|
weekStartsOn = 1,
|
|
177
182
|
calendarStyle,
|
|
183
|
+
hideTrigger = false,
|
|
178
184
|
style,
|
|
179
185
|
className,
|
|
180
186
|
accessibilityLabel,
|
|
@@ -188,6 +194,10 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
188
194
|
} = props;
|
|
189
195
|
|
|
190
196
|
const isRange = props.range === true;
|
|
197
|
+
const usesRangeTrigger = isRange && variant !== "default";
|
|
198
|
+
const formatValue =
|
|
199
|
+
formatValueProp ??
|
|
200
|
+
(usesRangeTrigger ? (date: Date) => formatDateDayShortMonth(date, monthNames) : formatDate);
|
|
191
201
|
const valueProp = props.value;
|
|
192
202
|
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
193
203
|
const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
|
|
@@ -204,6 +214,8 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
204
214
|
);
|
|
205
215
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
206
216
|
const [hoveredDayKey, setHoveredDayKey] = useState<number | null>(null);
|
|
217
|
+
/** Web: flip calendar to the trigger's right edge when it would overflow the viewport. */
|
|
218
|
+
const [menuAlignEnd, setMenuAlignEnd] = useState(false);
|
|
207
219
|
|
|
208
220
|
const rangeValue: DateRange = isRange
|
|
209
221
|
? isValueControlled
|
|
@@ -218,6 +230,13 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
218
230
|
|
|
219
231
|
const isOpen = isOpenControlled ? openProp : uncontrolledOpen;
|
|
220
232
|
|
|
233
|
+
const defaultMenuWidth = usesRangeTrigger ? RANGE_TRIGGER_WIDTH : DEFAULT_WIDTH;
|
|
234
|
+
const resolvedMenuWidth = useMemo(() => {
|
|
235
|
+
const flat = StyleSheet.flatten(calendarStyle);
|
|
236
|
+
const width = flat?.width;
|
|
237
|
+
return typeof width === "number" ? width : defaultMenuWidth;
|
|
238
|
+
}, [calendarStyle, defaultMenuWidth]);
|
|
239
|
+
|
|
221
240
|
const [visibleMonth, setVisibleMonth] = useState(() => {
|
|
222
241
|
const anchor =
|
|
223
242
|
defaultVisibleMonth ?? (isRange ? rangeValue.start : singleValue) ?? new Date();
|
|
@@ -235,11 +254,33 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
235
254
|
setVisibleMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1));
|
|
236
255
|
} else {
|
|
237
256
|
setHoveredDayKey(null);
|
|
257
|
+
setMenuAlignEnd(false);
|
|
238
258
|
}
|
|
239
259
|
},
|
|
240
260
|
[isOpenControlled, isRange, onOpenChange, rangeValue.start, singleValue],
|
|
241
261
|
);
|
|
242
262
|
|
|
263
|
+
// Prefer left alignment; flip to right when the menu would overflow the viewport (web).
|
|
264
|
+
useLayoutEffect(() => {
|
|
265
|
+
if (Platform.OS !== "web" || !isOpen || hideTrigger) return;
|
|
266
|
+
|
|
267
|
+
const updateMenuAlign = () => {
|
|
268
|
+
const trigger = triggerRef.current as unknown as {
|
|
269
|
+
getBoundingClientRect?: () => DOMRect;
|
|
270
|
+
} | null;
|
|
271
|
+
const rect = trigger?.getBoundingClientRect?.();
|
|
272
|
+
if (!rect) return;
|
|
273
|
+
|
|
274
|
+
const viewportPadding = 8;
|
|
275
|
+
const overflowsRight = rect.left + resolvedMenuWidth > window.innerWidth - viewportPadding;
|
|
276
|
+
setMenuAlignEnd(overflowsRight);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
updateMenuAlign();
|
|
280
|
+
window.addEventListener("resize", updateMenuAlign);
|
|
281
|
+
return () => window.removeEventListener("resize", updateMenuAlign);
|
|
282
|
+
}, [hideTrigger, isOpen, resolvedMenuWidth]);
|
|
283
|
+
|
|
243
284
|
// Close on outside click (web only; native has no document-level events).
|
|
244
285
|
useEffect(() => {
|
|
245
286
|
if (Platform.OS !== "web" || !isOpen) return;
|
|
@@ -347,7 +388,6 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
347
388
|
? formatValue(singleValue)
|
|
348
389
|
: placeholder;
|
|
349
390
|
|
|
350
|
-
const usesRangeTrigger = isRange && variant !== "default";
|
|
351
391
|
const usesVerticalRangeTrigger = usesRangeTrigger && variant === "range-vertical";
|
|
352
392
|
const triggerBorderColor = usesRangeTrigger
|
|
353
393
|
? isOpen
|
|
@@ -369,6 +409,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
369
409
|
style={[
|
|
370
410
|
styles.root,
|
|
371
411
|
usesRangeTrigger && styles.rangeRoot,
|
|
412
|
+
hideTrigger && styles.rootHideTrigger,
|
|
372
413
|
isOpen && styles.rootOpen,
|
|
373
414
|
disabled && styles.disabled,
|
|
374
415
|
style,
|
|
@@ -376,52 +417,65 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
|
|
|
376
417
|
accessibilityState={{ disabled, expanded: isOpen }}
|
|
377
418
|
>
|
|
378
419
|
<View style={[styles.triggerWrap, isOpen && styles.triggerWrapOpen]}>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
{
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
420
|
+
{hideTrigger ? null : (
|
|
421
|
+
<Pressable
|
|
422
|
+
ref={triggerRef}
|
|
423
|
+
onPress={handleTriggerPress}
|
|
424
|
+
disabled={disabled}
|
|
425
|
+
accessibilityRole="button"
|
|
426
|
+
accessibilityLabel={resolvedAccessibilityLabel}
|
|
427
|
+
accessibilityState={{ disabled, expanded: isOpen }}
|
|
428
|
+
style={[
|
|
429
|
+
styles.trigger,
|
|
430
|
+
usesRangeTrigger && styles.rangeTrigger,
|
|
431
|
+
usesRangeTrigger &&
|
|
432
|
+
(usesVerticalRangeTrigger
|
|
433
|
+
? styles.rangeTriggerVertical
|
|
434
|
+
: styles.rangeTriggerHorizontal),
|
|
435
|
+
{
|
|
436
|
+
borderColor: triggerBorderColor,
|
|
437
|
+
backgroundColor: usesRangeTrigger ? "transparent" : colors.grey700,
|
|
438
|
+
},
|
|
439
|
+
]}
|
|
440
|
+
>
|
|
441
|
+
{usesRangeTrigger ? (
|
|
442
|
+
<>
|
|
443
|
+
<RangeTriggerField label={startLabel} time={startTime} value={startValue} />
|
|
444
|
+
<View
|
|
445
|
+
style={
|
|
446
|
+
usesVerticalRangeTrigger
|
|
447
|
+
? styles.rangeDividerVertical
|
|
448
|
+
: styles.rangeDividerHorizontal
|
|
449
|
+
}
|
|
450
|
+
/>
|
|
451
|
+
<RangeTriggerField label={endLabel} time={endTime} value={endValue} />
|
|
452
|
+
</>
|
|
453
|
+
) : (
|
|
454
|
+
<>
|
|
455
|
+
<Text style={[styles.triggerText, { color: triggerTextColor }]} numberOfLines={1}>
|
|
456
|
+
{triggerLabel}
|
|
457
|
+
</Text>
|
|
458
|
+
<View style={styles.iconSlot}>
|
|
459
|
+
<CalendarOutlineIcon size={ICON_SIZE} color={colors.grey100} />
|
|
460
|
+
</View>
|
|
461
|
+
</>
|
|
462
|
+
)}
|
|
463
|
+
</Pressable>
|
|
464
|
+
)}
|
|
422
465
|
|
|
423
466
|
{isOpen ? (
|
|
424
|
-
<View
|
|
467
|
+
<View
|
|
468
|
+
style={[
|
|
469
|
+
styles.menu,
|
|
470
|
+
usesRangeTrigger && styles.rangeMenu,
|
|
471
|
+
hideTrigger
|
|
472
|
+
? styles.menuInline
|
|
473
|
+
: menuAlignEnd
|
|
474
|
+
? styles.menuAlignEnd
|
|
475
|
+
: styles.menuAlignStart,
|
|
476
|
+
calendarStyle,
|
|
477
|
+
]}
|
|
478
|
+
>
|
|
425
479
|
<View style={styles.calendarHeader}>
|
|
426
480
|
<Pressable
|
|
427
481
|
onPress={handlePrevMonth}
|
|
@@ -588,6 +642,10 @@ const styles = StyleSheet.create({
|
|
|
588
642
|
gap: 8,
|
|
589
643
|
alignSelf: "flex-start",
|
|
590
644
|
},
|
|
645
|
+
rootHideTrigger: {
|
|
646
|
+
width: "auto",
|
|
647
|
+
gap: 0,
|
|
648
|
+
},
|
|
591
649
|
rootOpen: {
|
|
592
650
|
zIndex: 30,
|
|
593
651
|
},
|
|
@@ -641,7 +699,7 @@ const styles = StyleSheet.create({
|
|
|
641
699
|
justifyContent: "center",
|
|
642
700
|
gap: 8,
|
|
643
701
|
paddingVertical: 12,
|
|
644
|
-
paddingHorizontal:
|
|
702
|
+
paddingHorizontal: 12,
|
|
645
703
|
},
|
|
646
704
|
rangeLabel: {
|
|
647
705
|
...datePickerTypography.rangeLabel,
|
|
@@ -651,13 +709,16 @@ const styles = StyleSheet.create({
|
|
|
651
709
|
minWidth: 0,
|
|
652
710
|
flexDirection: "row",
|
|
653
711
|
alignItems: "center",
|
|
654
|
-
justifyContent: "
|
|
655
|
-
|
|
712
|
+
justifyContent: "flex-start",
|
|
713
|
+
flexWrap: "nowrap",
|
|
714
|
+
gap: 10,
|
|
656
715
|
},
|
|
657
716
|
rangeDate: {
|
|
658
717
|
...datePickerTypography.rangeDate,
|
|
659
|
-
flexShrink:
|
|
718
|
+
flexShrink: 0,
|
|
660
719
|
color: colors.white,
|
|
720
|
+
// @ts-expect-error RN Web — keep the full date on one line
|
|
721
|
+
whiteSpace: "nowrap",
|
|
661
722
|
},
|
|
662
723
|
rangeTimeWrap: {
|
|
663
724
|
flexShrink: 0,
|
|
@@ -689,7 +750,6 @@ const styles = StyleSheet.create({
|
|
|
689
750
|
menu: {
|
|
690
751
|
position: "absolute",
|
|
691
752
|
top: "100%",
|
|
692
|
-
left: 0,
|
|
693
753
|
width: DEFAULT_WIDTH,
|
|
694
754
|
marginTop: 8,
|
|
695
755
|
zIndex: 10,
|
|
@@ -706,6 +766,21 @@ const styles = StyleSheet.create({
|
|
|
706
766
|
},
|
|
707
767
|
}),
|
|
708
768
|
},
|
|
769
|
+
menuAlignStart: {
|
|
770
|
+
left: 0,
|
|
771
|
+
right: "auto",
|
|
772
|
+
},
|
|
773
|
+
menuAlignEnd: {
|
|
774
|
+
left: "auto",
|
|
775
|
+
right: 0,
|
|
776
|
+
},
|
|
777
|
+
menuInline: {
|
|
778
|
+
position: "relative",
|
|
779
|
+
top: "auto",
|
|
780
|
+
left: "auto",
|
|
781
|
+
right: "auto",
|
|
782
|
+
marginTop: 0,
|
|
783
|
+
},
|
|
709
784
|
rangeMenu: {
|
|
710
785
|
width: RANGE_TRIGGER_WIDTH,
|
|
711
786
|
},
|