ferns-ui 0.36.4 → 0.37.0
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/dist/Banner.d.ts +6 -16
- package/dist/Banner.js +52 -43
- package/dist/Banner.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/useStoredState.d.ts +1 -0
- package/dist/useStoredState.js +33 -0
- package/dist/useStoredState.js.map +1 -0
- package/package.json +55 -56
- package/src/ActionSheet.tsx +1231 -0
- package/src/Avatar.tsx +317 -0
- package/src/Badge.tsx +65 -0
- package/src/Banner.tsx +149 -0
- package/src/BlurBox.native.tsx +40 -0
- package/src/BlurBox.tsx +31 -0
- package/src/Body.tsx +32 -0
- package/src/Box.tsx +308 -0
- package/src/Button.tsx +219 -0
- package/src/Card.tsx +23 -0
- package/src/CheckBox.tsx +118 -0
- package/src/Common.ts +2743 -0
- package/src/Constants.ts +53 -0
- package/src/CustomSelect.tsx +85 -0
- package/src/DateTimeActionSheet.tsx +409 -0
- package/src/DateTimeField.android.tsx +101 -0
- package/src/DateTimeField.ios.tsx +83 -0
- package/src/DateTimeField.tsx +69 -0
- package/src/DecimalRangeActionSheet.tsx +113 -0
- package/src/ErrorBoundary.tsx +37 -0
- package/src/ErrorPage.tsx +44 -0
- package/src/FernsProvider.tsx +21 -0
- package/src/Field.tsx +299 -0
- package/src/FieldWithLabels.tsx +36 -0
- package/src/FlatList.tsx +2 -0
- package/src/Form.tsx +182 -0
- package/src/HeaderButtons.tsx +107 -0
- package/src/Heading.tsx +53 -0
- package/src/HeightActionSheet.tsx +104 -0
- package/src/Hyperlink.tsx +181 -0
- package/src/Icon.tsx +24 -0
- package/src/IconButton.tsx +165 -0
- package/src/Image.tsx +50 -0
- package/src/ImageBackground.tsx +14 -0
- package/src/InfoTooltipButton.tsx +23 -0
- package/src/Layer.tsx +17 -0
- package/src/Link.tsx +17 -0
- package/src/Mask.tsx +21 -0
- package/src/MediaQuery.ts +46 -0
- package/src/Meta.tsx +9 -0
- package/src/Modal.tsx +248 -0
- package/src/ModalSheet.tsx +58 -0
- package/src/NumberPickerActionSheet.tsx +66 -0
- package/src/Page.tsx +133 -0
- package/src/Permissions.ts +44 -0
- package/src/PickerSelect.tsx +553 -0
- package/src/Pill.tsx +24 -0
- package/src/Pog.tsx +87 -0
- package/src/ProgressBar.tsx +55 -0
- package/src/ScrollView.tsx +2 -0
- package/src/SegmentedControl.tsx +102 -0
- package/src/SelectList.tsx +89 -0
- package/src/SideDrawer.tsx +62 -0
- package/src/Spinner.tsx +20 -0
- package/src/SplitPage.native.tsx +160 -0
- package/src/SplitPage.tsx +302 -0
- package/src/Switch.tsx +19 -0
- package/src/Table.tsx +87 -0
- package/src/TableHeader.tsx +36 -0
- package/src/TableHeaderCell.tsx +76 -0
- package/src/TableRow.tsx +87 -0
- package/src/TapToEdit.tsx +221 -0
- package/src/Text.tsx +131 -0
- package/src/TextArea.tsx +16 -0
- package/src/TextField.tsx +401 -0
- package/src/TextFieldNumberActionSheet.tsx +61 -0
- package/src/Toast.tsx +106 -0
- package/src/Tooltip.tsx +269 -0
- package/src/UnifiedScreens.ts +24 -0
- package/src/Unifier.ts +371 -0
- package/src/Utilities.tsx +159 -0
- package/src/WithLabel.tsx +57 -0
- package/src/dayjsExtended.ts +10 -0
- package/src/index.tsx +1347 -0
- package/src/polyfill.d.ts +11 -0
- package/src/tableContext.tsx +80 -0
- package/src/useStoredState.ts +39 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, {forwardRef, useState} from "react";
|
|
2
|
+
import {Platform, Pressable, View, ViewStyle} from "react-native";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AllColors,
|
|
6
|
+
ButtonColor,
|
|
7
|
+
Color,
|
|
8
|
+
IconName,
|
|
9
|
+
IconPrefix,
|
|
10
|
+
IconSize,
|
|
11
|
+
iconSizeToNumber,
|
|
12
|
+
IndicatorDirection,
|
|
13
|
+
ThemeColor,
|
|
14
|
+
TooltipDirection,
|
|
15
|
+
} from "./Common";
|
|
16
|
+
import {Icon} from "./Icon";
|
|
17
|
+
import {Modal} from "./Modal";
|
|
18
|
+
import {Text} from "./Text";
|
|
19
|
+
import {Tooltip} from "./Tooltip";
|
|
20
|
+
import {Unifier} from "./Unifier";
|
|
21
|
+
|
|
22
|
+
export interface IconButtonProps {
|
|
23
|
+
prefix?: IconPrefix;
|
|
24
|
+
icon: IconName;
|
|
25
|
+
accessibilityLabel: string;
|
|
26
|
+
iconColor: "darkGray" | ButtonColor | ThemeColor | Color;
|
|
27
|
+
onClick: () => void;
|
|
28
|
+
size?: IconSize;
|
|
29
|
+
bgColor?: "transparent" | "transparentDarkGray" | "gray" | "lightGray" | "white"; // default transparent
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
selected?: boolean;
|
|
32
|
+
withConfirmation?: boolean;
|
|
33
|
+
confirmationText?: string;
|
|
34
|
+
confirmationHeading?: string;
|
|
35
|
+
tooltip?: {
|
|
36
|
+
text: string;
|
|
37
|
+
idealDirection?: TooltipDirection;
|
|
38
|
+
};
|
|
39
|
+
indicator?: boolean;
|
|
40
|
+
indicatorStyle?: {position: IndicatorDirection; color: AllColors};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// eslint-disable-next-line react/display-name
|
|
44
|
+
export const IconButton = forwardRef(
|
|
45
|
+
(
|
|
46
|
+
{
|
|
47
|
+
prefix,
|
|
48
|
+
icon,
|
|
49
|
+
iconColor,
|
|
50
|
+
onClick,
|
|
51
|
+
size,
|
|
52
|
+
bgColor = "transparent",
|
|
53
|
+
withConfirmation = false,
|
|
54
|
+
confirmationText = "Are you sure you want to continue?",
|
|
55
|
+
confirmationHeading = "Confirm",
|
|
56
|
+
tooltip,
|
|
57
|
+
indicator,
|
|
58
|
+
indicatorStyle = {position: "bottomRight", color: "primary"},
|
|
59
|
+
}: IconButtonProps,
|
|
60
|
+
ref
|
|
61
|
+
) => {
|
|
62
|
+
const [showConfirmation, setShowConfirmation] = useState(false);
|
|
63
|
+
|
|
64
|
+
const opacity = 1;
|
|
65
|
+
let color: string;
|
|
66
|
+
if (bgColor === "transparentDarkGray") {
|
|
67
|
+
color = "rgba(0, 0, 0, 0.5)";
|
|
68
|
+
} else if (bgColor === "transparent" || !bgColor) {
|
|
69
|
+
color = "rgba(0, 0, 0, 0.0)";
|
|
70
|
+
} else {
|
|
71
|
+
color = Unifier.theme[bgColor];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const IndicatorPosition = {
|
|
75
|
+
bottomRight: {bottom: "20%", right: "20%"},
|
|
76
|
+
bottomLeft: {bottom: "20%", left: "20%"},
|
|
77
|
+
topRight: {top: "20%", right: "20%"},
|
|
78
|
+
topLeft: {top: "20%", left: "20%"},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const indicatorPosition = {position: "absolute", ...IndicatorPosition[indicatorStyle.position]};
|
|
82
|
+
|
|
83
|
+
const renderConfirmation = () => {
|
|
84
|
+
return (
|
|
85
|
+
<Modal
|
|
86
|
+
heading={confirmationHeading}
|
|
87
|
+
primaryButtonOnClick={() => {
|
|
88
|
+
onClick();
|
|
89
|
+
setShowConfirmation(false);
|
|
90
|
+
}}
|
|
91
|
+
primaryButtonText="Confirm"
|
|
92
|
+
secondaryButtonOnClick={(): void => setShowConfirmation(false)}
|
|
93
|
+
secondaryButtonText="Cancel"
|
|
94
|
+
size="sm"
|
|
95
|
+
visible={showConfirmation}
|
|
96
|
+
onDismiss={(): void => {
|
|
97
|
+
setShowConfirmation(false);
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<Text>{confirmationText}</Text>
|
|
101
|
+
</Modal>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function renderIconButton(): React.ReactElement {
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<Pressable
|
|
109
|
+
ref={ref as any}
|
|
110
|
+
hitSlop={{top: 10, left: 10, bottom: 10, right: 10}}
|
|
111
|
+
style={{
|
|
112
|
+
opacity,
|
|
113
|
+
backgroundColor: color,
|
|
114
|
+
borderRadius: 100,
|
|
115
|
+
// paddingBottom: iconSizeToNumber(size) / 4,
|
|
116
|
+
// paddingTop: iconSizeToNumber(size) / 4,
|
|
117
|
+
// paddingLeft: iconSizeToNumber(size) / 2,
|
|
118
|
+
// paddingRight: iconSizeToNumber(size) / 2,
|
|
119
|
+
width: iconSizeToNumber(size) * 2.5,
|
|
120
|
+
height: iconSizeToNumber(size) * 2.5,
|
|
121
|
+
display: "flex",
|
|
122
|
+
justifyContent: "center",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
}}
|
|
125
|
+
onPress={() => {
|
|
126
|
+
Unifier.utils.haptic();
|
|
127
|
+
if (withConfirmation && !showConfirmation) {
|
|
128
|
+
setShowConfirmation(true);
|
|
129
|
+
} else if (onClick) {
|
|
130
|
+
onClick();
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<Icon color={iconColor} name={icon} prefix={prefix || "fas"} size={size} />
|
|
135
|
+
{indicator && (
|
|
136
|
+
<View style={indicatorPosition as ViewStyle}>
|
|
137
|
+
<Icon
|
|
138
|
+
color={indicatorStyle.color}
|
|
139
|
+
name="circle"
|
|
140
|
+
prefix={prefix || "fas"}
|
|
141
|
+
size="sm"
|
|
142
|
+
/>
|
|
143
|
+
</View>
|
|
144
|
+
)}
|
|
145
|
+
</Pressable>
|
|
146
|
+
|
|
147
|
+
{Boolean(withConfirmation) && renderConfirmation()}
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Only add for web. This doesn't make much sense for mobile, since the action would be performed for the button
|
|
153
|
+
// as well as the tooltip appearing.
|
|
154
|
+
// TODO: Add tooltip info button next to the icon button on mobile.
|
|
155
|
+
if (tooltip && Platform.OS === "web") {
|
|
156
|
+
return (
|
|
157
|
+
<Tooltip idealDirection={tooltip.idealDirection} text={tooltip.text}>
|
|
158
|
+
{renderIconButton()}
|
|
159
|
+
</Tooltip>
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
return renderIconButton();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
);
|
package/src/Image.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {Dimensions, Image as NativeImage} from "react-native";
|
|
3
|
+
|
|
4
|
+
import {Box} from "./Box";
|
|
5
|
+
import {ImageProps} from "./Common";
|
|
6
|
+
const {width: DEVICE_WIDTH} = Dimensions.get("window");
|
|
7
|
+
|
|
8
|
+
export class Image extends React.Component<ImageProps, {}> {
|
|
9
|
+
resizeMode = (fit?: "cover" | "contain" | "none") => {
|
|
10
|
+
if (!fit || fit === "none") {
|
|
11
|
+
return undefined;
|
|
12
|
+
} else {
|
|
13
|
+
return fit;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
width = () => {
|
|
18
|
+
if (this.props.naturalWidth) {
|
|
19
|
+
return this.props.naturalWidth;
|
|
20
|
+
} else if (this.props.fullWidth) {
|
|
21
|
+
return DEVICE_WIDTH;
|
|
22
|
+
}
|
|
23
|
+
throw new Error("Width required for Image");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
height = () => {
|
|
27
|
+
if (this.props.naturalWidth) {
|
|
28
|
+
return this.props.naturalWidth;
|
|
29
|
+
}
|
|
30
|
+
return this.width() * (9 / 16);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
return (
|
|
35
|
+
<Box color={this.props.color}>
|
|
36
|
+
<NativeImage
|
|
37
|
+
resizeMode={this.resizeMode(this.props.fit)}
|
|
38
|
+
source={{uri: this.props.src, cache: "force-cache"}}
|
|
39
|
+
style={{
|
|
40
|
+
height: this.height(),
|
|
41
|
+
width: this.width(),
|
|
42
|
+
maxHeight: this.props.maxHeight,
|
|
43
|
+
maxWidth: this.props.maxWidth,
|
|
44
|
+
...this.props.style,
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {ImageBackground as ImageBackgroundNative} from "react-native";
|
|
3
|
+
|
|
4
|
+
interface ImageBackgroundProps {
|
|
5
|
+
children?: any;
|
|
6
|
+
style?: any;
|
|
7
|
+
source: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ImageBackground extends React.Component<ImageBackgroundProps, {}> {
|
|
11
|
+
render() {
|
|
12
|
+
return <ImageBackgroundNative {...this.props} />;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {IconSize} from "./Common";
|
|
4
|
+
import {IconButton} from "./IconButton";
|
|
5
|
+
|
|
6
|
+
interface InfoTooltipButtonProps {
|
|
7
|
+
text: string;
|
|
8
|
+
size?: IconSize;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function InfoTooltipButton({text, size}: InfoTooltipButtonProps): React.ReactElement {
|
|
12
|
+
return (
|
|
13
|
+
<IconButton
|
|
14
|
+
accessibilityLabel="info"
|
|
15
|
+
bgColor="transparent"
|
|
16
|
+
icon="exclamation"
|
|
17
|
+
iconColor="blue"
|
|
18
|
+
size={size}
|
|
19
|
+
tooltip={{text}}
|
|
20
|
+
onClick={() => {}}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
package/src/Layer.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {LayerProps} from "./Common";
|
|
4
|
+
|
|
5
|
+
interface LayerState {}
|
|
6
|
+
|
|
7
|
+
// TODO: Flesh out for native.
|
|
8
|
+
export class Layer extends React.Component<LayerProps, LayerState> {
|
|
9
|
+
constructor(props: LayerProps) {
|
|
10
|
+
super(props);
|
|
11
|
+
this.state = {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
render() {
|
|
15
|
+
return this.props.children;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/Link.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {LinkProps} from "./Common";
|
|
4
|
+
import {Text} from "./Text";
|
|
5
|
+
|
|
6
|
+
interface LinkState {}
|
|
7
|
+
|
|
8
|
+
export class Link extends React.Component<LinkProps, LinkState> {
|
|
9
|
+
constructor(props: LinkProps) {
|
|
10
|
+
super(props);
|
|
11
|
+
this.state = {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
render() {
|
|
15
|
+
return <Text>{this.props.children}</Text>;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/Mask.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {View} from "react-native";
|
|
3
|
+
|
|
4
|
+
import {MaskProps, ReactChildren} from "./Common";
|
|
5
|
+
|
|
6
|
+
export function Mask(props: MaskProps): ReactChildren {
|
|
7
|
+
if (props.shape === "rounded") {
|
|
8
|
+
return <View style={{overflow: "hidden", borderRadius: 12}}>{props.children}</View>;
|
|
9
|
+
} else if (props.shape === "circle") {
|
|
10
|
+
return <View style={{overflow: "hidden", borderRadius: 1000}}>{props.children}</View>;
|
|
11
|
+
}
|
|
12
|
+
if (props.rounding) {
|
|
13
|
+
const rounding = props.rounding === "circle" ? 100 : props.rounding;
|
|
14
|
+
// Subtract 1 from rounding because of some very odd rendering.
|
|
15
|
+
return (
|
|
16
|
+
<View style={{borderRadius: (rounding - 1) * 4, overflow: "visible"}}>{props.children}</View>
|
|
17
|
+
);
|
|
18
|
+
} else {
|
|
19
|
+
return props.children || null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {Dimensions} from "react-native";
|
|
2
|
+
|
|
3
|
+
export function mediaQuery(): "xs" | "sm" | "md" | "lg" {
|
|
4
|
+
const width = Dimensions.get("window").width;
|
|
5
|
+
if (width < 576) {
|
|
6
|
+
return "xs";
|
|
7
|
+
} else if (width < 768) {
|
|
8
|
+
return "sm";
|
|
9
|
+
} else if (width < 1312) {
|
|
10
|
+
return "md";
|
|
11
|
+
} else {
|
|
12
|
+
return "lg";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function mediaQueryLargerThan(size: "xs" | "sm" | "md" | "lg"): boolean {
|
|
17
|
+
const media = mediaQuery();
|
|
18
|
+
if (size === "xs") {
|
|
19
|
+
return true;
|
|
20
|
+
} else if (size === "sm") {
|
|
21
|
+
return ["sm", "md", "lg"].includes(media);
|
|
22
|
+
} else if (size === "md") {
|
|
23
|
+
return ["md", "lg"].includes(media);
|
|
24
|
+
} else if (size === "lg") {
|
|
25
|
+
return ["lg"].includes(media);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function mediaQuerySmallerThan(size: "xs" | "sm" | "md" | "lg"): boolean {
|
|
31
|
+
const media = mediaQuery();
|
|
32
|
+
if (size === "lg") {
|
|
33
|
+
return true;
|
|
34
|
+
} else if (size === "md") {
|
|
35
|
+
return ["xs", "sm", "md"].includes(media);
|
|
36
|
+
} else if (size === "sm") {
|
|
37
|
+
return ["xs", "sm"].includes(media);
|
|
38
|
+
} else if (size === "xs") {
|
|
39
|
+
return ["xs"].includes(media);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isMobileDevice(): boolean {
|
|
45
|
+
return !mediaQueryLargerThan("sm");
|
|
46
|
+
}
|
package/src/Meta.tsx
ADDED
package/src/Modal.tsx
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import React, {useEffect, useRef} from "react";
|
|
2
|
+
import {Dimensions, Modal as RNModal} from "react-native";
|
|
3
|
+
import ActionSheet, {ActionSheetRef} from "react-native-actions-sheet";
|
|
4
|
+
|
|
5
|
+
import {Box} from "./Box";
|
|
6
|
+
import {Button} from "./Button";
|
|
7
|
+
import {Heading} from "./Heading";
|
|
8
|
+
import {IconButton} from "./IconButton";
|
|
9
|
+
import {isMobileDevice} from "./MediaQuery";
|
|
10
|
+
import {Text} from "./Text";
|
|
11
|
+
|
|
12
|
+
interface ModalProps {
|
|
13
|
+
onDismiss: () => void;
|
|
14
|
+
visible: boolean;
|
|
15
|
+
// Alignment of the header. Default is "center".
|
|
16
|
+
align?: "center" | "start";
|
|
17
|
+
// Element to render in the middle part of the modal.
|
|
18
|
+
children?: React.ReactElement;
|
|
19
|
+
// Element to render in the bottom of the modal. This takes precedence over primaryButton and secondaryButton.
|
|
20
|
+
footer?: React.ReactElement;
|
|
21
|
+
heading?: string;
|
|
22
|
+
size?: "sm" | "md" | "lg";
|
|
23
|
+
subHeading?: string;
|
|
24
|
+
// Renders a primary colored button all the way to the right in the footer, if no footer prop is provided.
|
|
25
|
+
primaryButtonText?: string;
|
|
26
|
+
primaryButtonOnClick?: (value?: any) => void;
|
|
27
|
+
primaryButtonDisabled?: boolean;
|
|
28
|
+
// Renders a gray button to the left of the primary button in the footer, if no footer prop is provided.
|
|
29
|
+
// Requires primaryButtonText to be defined, but is not required itself.
|
|
30
|
+
secondaryButtonText?: string;
|
|
31
|
+
secondaryButtonOnClick?: (value?: any) => void;
|
|
32
|
+
// Whether to show a close button in the upper left of modals or action sheets.
|
|
33
|
+
showClose?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Modal = ({
|
|
37
|
+
onDismiss,
|
|
38
|
+
visible,
|
|
39
|
+
align = "center",
|
|
40
|
+
children,
|
|
41
|
+
footer,
|
|
42
|
+
heading,
|
|
43
|
+
size,
|
|
44
|
+
subHeading,
|
|
45
|
+
primaryButtonText,
|
|
46
|
+
primaryButtonOnClick,
|
|
47
|
+
primaryButtonDisabled = false,
|
|
48
|
+
secondaryButtonText,
|
|
49
|
+
secondaryButtonOnClick,
|
|
50
|
+
showClose = false,
|
|
51
|
+
}: ModalProps): React.ReactElement => {
|
|
52
|
+
const actionSheetRef = useRef<ActionSheetRef>(null);
|
|
53
|
+
|
|
54
|
+
if (subHeading && !heading) {
|
|
55
|
+
throw new Error("Cannot render Modal with subHeading and no heading");
|
|
56
|
+
}
|
|
57
|
+
if (!footer && !primaryButtonText && !secondaryButtonText && !showClose) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Cannot render Modal without footer, primaryButtonText, secondaryButtonText, or showClose"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let sizePx: string | number = 540;
|
|
64
|
+
if (size === "md") {
|
|
65
|
+
sizePx = 720;
|
|
66
|
+
} else if (size === "lg") {
|
|
67
|
+
sizePx = 900;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Adjust size for small screens
|
|
71
|
+
if (sizePx > Dimensions.get("window").width) {
|
|
72
|
+
sizePx = "90%";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Modal uses a visible prop, but ActionSheet uses a setModalVisible method on a reference.
|
|
76
|
+
// Open the action sheet ref when the visible prop changes.
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (actionSheetRef.current) {
|
|
79
|
+
actionSheetRef.current.setModalVisible(visible);
|
|
80
|
+
}
|
|
81
|
+
}, [visible, actionSheetRef]);
|
|
82
|
+
|
|
83
|
+
const renderClose = (): React.ReactElement | null => {
|
|
84
|
+
if (!showClose) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
88
|
+
<IconButton
|
|
89
|
+
accessibilityLabel="close"
|
|
90
|
+
bgColor="white"
|
|
91
|
+
icon="times"
|
|
92
|
+
iconColor="darkGray"
|
|
93
|
+
onClick={() => onDismiss()}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const renderModalHeader = (): React.ReactElement => {
|
|
99
|
+
return (
|
|
100
|
+
<Box direction="row" padding={3} width="100%">
|
|
101
|
+
<Box width={40}>{renderClose()}</Box>
|
|
102
|
+
<Box direction="column" flex="grow">
|
|
103
|
+
<Heading align={align === "center" ? "center" : undefined} size="sm">
|
|
104
|
+
{heading}
|
|
105
|
+
</Heading>
|
|
106
|
+
{Boolean(subHeading) && (
|
|
107
|
+
<Box paddingY={2}>
|
|
108
|
+
<Text align={align === "center" ? "center" : undefined}>{subHeading}</Text>
|
|
109
|
+
</Box>
|
|
110
|
+
)}
|
|
111
|
+
</Box>
|
|
112
|
+
<Box width={40} />
|
|
113
|
+
</Box>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const renderModalFooter = (): React.ReactElement | null => {
|
|
118
|
+
if (footer) {
|
|
119
|
+
return footer;
|
|
120
|
+
}
|
|
121
|
+
return (
|
|
122
|
+
<Box direction="row" justifyContent="end" width="100%">
|
|
123
|
+
{Boolean(secondaryButtonText) && (
|
|
124
|
+
<Box marginRight={4} minWidth={120}>
|
|
125
|
+
<Button
|
|
126
|
+
color="gray"
|
|
127
|
+
text={secondaryButtonText ?? ""}
|
|
128
|
+
onClick={secondaryButtonOnClick}
|
|
129
|
+
/>
|
|
130
|
+
</Box>
|
|
131
|
+
)}
|
|
132
|
+
<Box minWidth={120}>
|
|
133
|
+
<Button
|
|
134
|
+
color="primary"
|
|
135
|
+
disabled={primaryButtonDisabled}
|
|
136
|
+
text={primaryButtonText ?? ""}
|
|
137
|
+
onClick={primaryButtonOnClick}
|
|
138
|
+
/>
|
|
139
|
+
</Box>
|
|
140
|
+
</Box>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const renderModal = (): React.ReactElement => {
|
|
145
|
+
return (
|
|
146
|
+
<RNModal animationType="slide" transparent visible={visible} onRequestClose={onDismiss}>
|
|
147
|
+
<Box
|
|
148
|
+
alignItems="center"
|
|
149
|
+
alignSelf="center"
|
|
150
|
+
color="white"
|
|
151
|
+
dangerouslySetInlineStyle={{
|
|
152
|
+
__style: {
|
|
153
|
+
zIndex: 1,
|
|
154
|
+
shadowColor: "#999",
|
|
155
|
+
shadowOffset: {
|
|
156
|
+
width: 4,
|
|
157
|
+
height: 6,
|
|
158
|
+
},
|
|
159
|
+
shadowRadius: 4,
|
|
160
|
+
shadowOpacity: 1.0,
|
|
161
|
+
elevation: 8,
|
|
162
|
+
},
|
|
163
|
+
}}
|
|
164
|
+
direction="column"
|
|
165
|
+
justifyContent="center"
|
|
166
|
+
marginTop={12}
|
|
167
|
+
maxWidth={sizePx}
|
|
168
|
+
minWidth={300}
|
|
169
|
+
paddingX={8}
|
|
170
|
+
paddingY={2}
|
|
171
|
+
rounding={6}
|
|
172
|
+
shadow
|
|
173
|
+
width={sizePx}
|
|
174
|
+
>
|
|
175
|
+
<Box marginBottom={6} width="100%">
|
|
176
|
+
{renderModalHeader()}
|
|
177
|
+
<Box paddingY={4}>{children}</Box>
|
|
178
|
+
<Box paddingY={4}>{renderModalFooter()}</Box>
|
|
179
|
+
</Box>
|
|
180
|
+
</Box>
|
|
181
|
+
</RNModal>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const renderActionSheet = (): React.ReactElement => {
|
|
186
|
+
return (
|
|
187
|
+
<ActionSheet ref={actionSheetRef} onClose={onDismiss}>
|
|
188
|
+
<Box direction="row" marginBottom={2} paddingX={2} paddingY={2} width="100%">
|
|
189
|
+
<Box marginRight={4}>
|
|
190
|
+
{Boolean(showClose) && (
|
|
191
|
+
<IconButton
|
|
192
|
+
accessibilityLabel="close"
|
|
193
|
+
bgColor="white"
|
|
194
|
+
icon="times"
|
|
195
|
+
iconColor="darkGray"
|
|
196
|
+
size="lg"
|
|
197
|
+
onClick={() => onDismiss()}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
{Boolean(secondaryButtonText) && (
|
|
201
|
+
<Button
|
|
202
|
+
color="darkGray"
|
|
203
|
+
inline
|
|
204
|
+
size="lg"
|
|
205
|
+
text={secondaryButtonText ?? ""}
|
|
206
|
+
type="ghost"
|
|
207
|
+
onClick={secondaryButtonOnClick}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
</Box>
|
|
211
|
+
<Box alignItems="center" direction="column" flex="grow" justifyContent="center">
|
|
212
|
+
<Heading align={align === "center" ? "center" : undefined} size="sm">
|
|
213
|
+
{heading}
|
|
214
|
+
</Heading>
|
|
215
|
+
{Boolean(subHeading) && (
|
|
216
|
+
<Box paddingY={2}>
|
|
217
|
+
<Text align={align === "center" ? "center" : undefined}>{subHeading}</Text>
|
|
218
|
+
</Box>
|
|
219
|
+
)}
|
|
220
|
+
</Box>
|
|
221
|
+
|
|
222
|
+
<Box alignSelf="start" height="100%" justifyContent="start">
|
|
223
|
+
{Boolean(primaryButtonText) && (
|
|
224
|
+
<Button
|
|
225
|
+
color="primary"
|
|
226
|
+
disabled={primaryButtonDisabled}
|
|
227
|
+
inline
|
|
228
|
+
size="md"
|
|
229
|
+
text={primaryButtonText!}
|
|
230
|
+
type="ghost"
|
|
231
|
+
onClick={primaryButtonOnClick}
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
</Box>
|
|
235
|
+
</Box>
|
|
236
|
+
<Box marginBottom={12} paddingX={4}>
|
|
237
|
+
{children}
|
|
238
|
+
</Box>
|
|
239
|
+
</ActionSheet>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (isMobileDevice()) {
|
|
244
|
+
return renderActionSheet();
|
|
245
|
+
} else {
|
|
246
|
+
return renderModal();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* eslint-disable react/display-name */
|
|
2
|
+
import React, {forwardRef, useEffect, useRef} from "react";
|
|
3
|
+
import {Animated} from "react-native";
|
|
4
|
+
import {Modalize} from "react-native-modalize";
|
|
5
|
+
import {Portal} from "react-native-portalize";
|
|
6
|
+
|
|
7
|
+
export const useCombinedRefs = (...refs: any) => {
|
|
8
|
+
const targetRef = useRef();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
refs.forEach((ref: any) => {
|
|
12
|
+
if (!ref) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof ref === "function") {
|
|
17
|
+
ref(targetRef.current);
|
|
18
|
+
} else {
|
|
19
|
+
ref.current = targetRef.current;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}, [refs]);
|
|
23
|
+
|
|
24
|
+
return targetRef;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
children: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const SimpleContent = forwardRef((props: Props, ref) => {
|
|
32
|
+
const modalizeRef = useRef(null);
|
|
33
|
+
const combinedRef = useCombinedRefs(ref, modalizeRef);
|
|
34
|
+
const animated = useRef(new Animated.Value(0)).current;
|
|
35
|
+
|
|
36
|
+
// const renderHeader = () => (
|
|
37
|
+
// <Box paddingY={4} marginTop={4} marginBottom={4}>
|
|
38
|
+
// <Text>50 users online</Text>
|
|
39
|
+
// </Box>
|
|
40
|
+
// );
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Portal>
|
|
44
|
+
<Modalize
|
|
45
|
+
// HeaderComponent={renderHeader}
|
|
46
|
+
ref={combinedRef}
|
|
47
|
+
adjustToContentHeight
|
|
48
|
+
panGestureAnimatedValue={animated}
|
|
49
|
+
scrollViewProps={{
|
|
50
|
+
showsVerticalScrollIndicator: false,
|
|
51
|
+
stickyHeaderIndices: [0],
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{props.children}
|
|
55
|
+
</Modalize>
|
|
56
|
+
</Portal>
|
|
57
|
+
);
|
|
58
|
+
});
|