ferns-ui 1.7.0 → 1.8.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/Common.d.ts +42 -0
- package/dist/DateTimeField.test.js +5 -9
- package/dist/DateTimeField.test.js.map +1 -1
- package/dist/Icon.js +2 -1
- package/dist/Icon.js.map +1 -1
- package/dist/Modal.js +21 -6
- package/dist/Modal.js.map +1 -1
- package/dist/SelectBadge.d.ts +3 -0
- package/dist/SelectBadge.js +180 -0
- package/dist/SelectBadge.js.map +1 -0
- package/dist/TextArea.test.d.ts +1 -0
- package/dist/TextArea.test.js +146 -0
- package/dist/TextArea.test.js.map +1 -0
- package/dist/TextField.test.d.ts +1 -0
- package/dist/TextField.test.js +251 -0
- package/dist/TextField.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test-utils.d.ts +97 -0
- package/dist/test-utils.js +23 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/useStoredState.d.ts +1 -1
- package/dist/useStoredState.js +19 -4
- package/dist/useStoredState.js.map +1 -1
- package/dist/useStoredState.test.d.ts +1 -0
- package/dist/useStoredState.test.js +93 -0
- package/dist/useStoredState.test.js.map +1 -0
- package/package.json +1 -1
- package/src/Common.ts +43 -0
- package/src/DateTimeField.test.tsx +5 -10
- package/src/Icon.tsx +1 -1
- package/src/Modal.tsx +24 -6
- package/src/SelectBadge.tsx +279 -0
- package/src/TextArea.test.tsx +271 -0
- package/src/TextField.test.tsx +442 -0
- package/src/__snapshots__/TextArea.test.tsx.snap +424 -0
- package/src/__snapshots__/TextField.test.tsx.snap +481 -0
- package/src/index.tsx +1 -0
- package/src/test-utils.tsx +26 -0
- package/src/useStoredState.test.tsx +134 -0
- package/src/useStoredState.ts +21 -5
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react-native";
|
|
2
|
+
import { Unifier } from "./Unifier";
|
|
3
|
+
import { useStoredState } from "./useStoredState";
|
|
4
|
+
jest.mock("./Unifier", () => ({
|
|
5
|
+
Unifier: {
|
|
6
|
+
storage: {
|
|
7
|
+
getItem: jest.fn(),
|
|
8
|
+
setItem: jest.fn(),
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
describe("useStoredState", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
it("should return initialValue and isLoading=true on initial render", async () => {
|
|
17
|
+
Unifier.storage.getItem.mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve("stored value"), 100)));
|
|
18
|
+
const { result } = renderHook(() => useStoredState("testKey", "initial value"));
|
|
19
|
+
expect(result.current[0]).toBe("initial value");
|
|
20
|
+
expect(result.current[2]).toBe(true);
|
|
21
|
+
await act(async () => {
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
23
|
+
});
|
|
24
|
+
expect(result.current[0]).toBe("stored value");
|
|
25
|
+
expect(result.current[2]).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it("should update state and storage when setter is called", async () => {
|
|
28
|
+
Unifier.storage.getItem.mockResolvedValue("stored value");
|
|
29
|
+
Unifier.storage.setItem.mockResolvedValue(undefined);
|
|
30
|
+
const { result } = renderHook(() => useStoredState("testKey", "initial value"));
|
|
31
|
+
await act(async () => {
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
33
|
+
});
|
|
34
|
+
await act(async () => {
|
|
35
|
+
await result.current[1]("new value");
|
|
36
|
+
});
|
|
37
|
+
expect(result.current[0]).toBe("new value");
|
|
38
|
+
expect(Unifier.storage.setItem).toHaveBeenCalledWith("testKey", "new value");
|
|
39
|
+
});
|
|
40
|
+
it("should handle errors when reading from storage", async () => {
|
|
41
|
+
const originalConsoleError = console.error;
|
|
42
|
+
console.error = jest.fn();
|
|
43
|
+
Unifier.storage.getItem.mockRejectedValue(new Error("Storage error"));
|
|
44
|
+
const { result } = renderHook(() => useStoredState("testKey", "initial value"));
|
|
45
|
+
await act(async () => {
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
47
|
+
});
|
|
48
|
+
expect(result.current[0]).toBe("initial value");
|
|
49
|
+
expect(result.current[2]).toBe(false);
|
|
50
|
+
expect(console.error).toHaveBeenCalled();
|
|
51
|
+
console.error = originalConsoleError;
|
|
52
|
+
});
|
|
53
|
+
it("should handle errors when writing to storage", async () => {
|
|
54
|
+
const originalConsoleError = console.error;
|
|
55
|
+
console.error = jest.fn();
|
|
56
|
+
Unifier.storage.getItem.mockResolvedValue("stored value");
|
|
57
|
+
Unifier.storage.setItem.mockRejectedValue(new Error("Storage error"));
|
|
58
|
+
const { result } = renderHook(() => useStoredState("testKey", "initial value"));
|
|
59
|
+
await act(async () => {
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
61
|
+
});
|
|
62
|
+
await act(async () => {
|
|
63
|
+
await result.current[1]("new value");
|
|
64
|
+
});
|
|
65
|
+
expect(result.current[0]).toBe("stored value");
|
|
66
|
+
expect(console.error).toHaveBeenCalled();
|
|
67
|
+
console.error = originalConsoleError;
|
|
68
|
+
});
|
|
69
|
+
it("should handle undefined initialValue", async () => {
|
|
70
|
+
Unifier.storage.getItem.mockResolvedValue(null);
|
|
71
|
+
const { result } = renderHook(() => useStoredState("testKey"));
|
|
72
|
+
expect(result.current[0]).toBeUndefined();
|
|
73
|
+
expect(result.current[2]).toBe(true);
|
|
74
|
+
await act(async () => {
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
76
|
+
});
|
|
77
|
+
expect(result.current[0]).toBeNull();
|
|
78
|
+
expect(result.current[2]).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
it("should not update state if component unmounts before storage resolves", async () => {
|
|
81
|
+
Unifier.storage.getItem.mockImplementation(() => new Promise((resolve) => setTimeout(() => resolve("stored value"), 100)));
|
|
82
|
+
const { result, unmount } = renderHook(() => useStoredState("testKey", "initial value"));
|
|
83
|
+
expect(result.current[0]).toBe("initial value");
|
|
84
|
+
expect(result.current[2]).toBe(true);
|
|
85
|
+
unmount();
|
|
86
|
+
await act(async () => {
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
88
|
+
});
|
|
89
|
+
expect(result.current[0]).toBe("initial value");
|
|
90
|
+
expect(result.current[2]).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
//# sourceMappingURL=useStoredState.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStoredState.test.js","sourceRoot":"","sources":["../src/useStoredState.test.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/Common.ts
CHANGED
|
@@ -1361,6 +1361,49 @@ export interface BadgeProps {
|
|
|
1361
1361
|
variant?: "iconOnly" | "numberOnly";
|
|
1362
1362
|
}
|
|
1363
1363
|
|
|
1364
|
+
export interface SelectBadgeProps {
|
|
1365
|
+
/**
|
|
1366
|
+
* When status is "custom", determines the badge's background color.
|
|
1367
|
+
*/
|
|
1368
|
+
customBackgroundColor?: string;
|
|
1369
|
+
/**
|
|
1370
|
+
* When status is "custom", determines the badge's border color.
|
|
1371
|
+
*/
|
|
1372
|
+
customBorderColor?: string;
|
|
1373
|
+
/**
|
|
1374
|
+
* When status is "custom", determines the badge's text color.
|
|
1375
|
+
*/
|
|
1376
|
+
customTextColor?: string;
|
|
1377
|
+
/**
|
|
1378
|
+
* If true, the badge will be disabled and not interactive.
|
|
1379
|
+
* @default false
|
|
1380
|
+
*/
|
|
1381
|
+
disabled?: boolean;
|
|
1382
|
+
/**
|
|
1383
|
+
* The options available for the dropdown badge.
|
|
1384
|
+
* Each option should have a label and a value.
|
|
1385
|
+
*/
|
|
1386
|
+
options: FieldOption[];
|
|
1387
|
+
/**
|
|
1388
|
+
* The current value of the select field.
|
|
1389
|
+
*/
|
|
1390
|
+
value?: string;
|
|
1391
|
+
/**
|
|
1392
|
+
* If true, the badge will have a secondary style.
|
|
1393
|
+
* @default false
|
|
1394
|
+
*/
|
|
1395
|
+
secondary?: boolean;
|
|
1396
|
+
/**
|
|
1397
|
+
* The status of the badge. Determines its color and appearance.
|
|
1398
|
+
* @default "info"
|
|
1399
|
+
*/
|
|
1400
|
+
status?: "info" | "error" | "warning" | "success" | "neutral" | "custom";
|
|
1401
|
+
/**
|
|
1402
|
+
* The function to call when the selected value changes.
|
|
1403
|
+
*/
|
|
1404
|
+
onChange: (value: string) => void;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1364
1407
|
type BannerButtonProps = {
|
|
1365
1408
|
/**
|
|
1366
1409
|
* Text to display on optional banner button, will display button if provided
|
|
@@ -1,26 +1,21 @@
|
|
|
1
|
-
import {act,
|
|
1
|
+
import {act, userEvent} from "@testing-library/react-native";
|
|
2
2
|
import {DateTime} from "luxon";
|
|
3
3
|
import React from "react";
|
|
4
4
|
|
|
5
5
|
import {DateTimeField} from "./DateTimeField";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const renderWithTheme = (ui: React.ReactElement) => {
|
|
9
|
-
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
10
|
-
};
|
|
6
|
+
import {renderWithTheme, setupComponentTest, teardownComponentTest} from "./test-utils";
|
|
11
7
|
|
|
12
8
|
describe("DateTimeField", () => {
|
|
13
9
|
let mockOnChange: jest.Mock;
|
|
14
10
|
|
|
15
11
|
beforeEach(() => {
|
|
16
|
-
|
|
12
|
+
const mocks = setupComponentTest();
|
|
17
13
|
jest.setSystemTime(new Date("2023-05-15T10:30:00.000Z"));
|
|
18
|
-
mockOnChange =
|
|
14
|
+
mockOnChange = mocks.onChange;
|
|
19
15
|
});
|
|
20
16
|
|
|
21
17
|
afterEach(() => {
|
|
22
|
-
|
|
23
|
-
jest.clearAllMocks();
|
|
18
|
+
teardownComponentTest();
|
|
24
19
|
});
|
|
25
20
|
|
|
26
21
|
describe("date type", () => {
|
package/src/Icon.tsx
CHANGED
|
@@ -15,7 +15,7 @@ export const Icon = ({
|
|
|
15
15
|
testID,
|
|
16
16
|
}: IconProps): React.ReactElement => {
|
|
17
17
|
const {theme} = useTheme();
|
|
18
|
-
const iconColor = theme.text[color];
|
|
18
|
+
const iconColor = theme.text[color] ?? color;
|
|
19
19
|
const iconSize = iconSizeToNumber(size);
|
|
20
20
|
return (
|
|
21
21
|
<FontAwesome6
|
package/src/Modal.tsx
CHANGED
|
@@ -178,9 +178,27 @@ export const Modal: FC<ModalProps> = ({
|
|
|
178
178
|
const actionSheetRef = useRef<ActionSheetRef>(null);
|
|
179
179
|
const {theme} = useTheme();
|
|
180
180
|
|
|
181
|
+
const handleDismiss = () => {
|
|
182
|
+
if (visible && onDismiss) {
|
|
183
|
+
onDismiss();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handlePrimaryButtonClick = (value?: Parameters<NonNullable<ModalProps["primaryButtonOnClick"]>>[0]) => {
|
|
188
|
+
if (visible && primaryButtonOnClick) {
|
|
189
|
+
return primaryButtonOnClick(value);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleSecondaryButtonClick = (value?: Parameters<NonNullable<ModalProps["secondaryButtonOnClick"]>>[0]) => {
|
|
194
|
+
if (visible && secondaryButtonOnClick) {
|
|
195
|
+
return secondaryButtonOnClick(value);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
181
199
|
const onHandlerStateChange = ({nativeEvent}: PanGestureHandlerStateChangeEvent) => {
|
|
182
200
|
if (nativeEvent.state === State.END && nativeEvent.translationY > 100) {
|
|
183
|
-
|
|
201
|
+
handleDismiss();
|
|
184
202
|
}
|
|
185
203
|
};
|
|
186
204
|
|
|
@@ -201,9 +219,9 @@ export const Modal: FC<ModalProps> = ({
|
|
|
201
219
|
primaryButtonText,
|
|
202
220
|
primaryButtonDisabled,
|
|
203
221
|
secondaryButtonText,
|
|
204
|
-
primaryButtonOnClick,
|
|
205
|
-
secondaryButtonOnClick,
|
|
206
|
-
onDismiss,
|
|
222
|
+
primaryButtonOnClick: handlePrimaryButtonClick,
|
|
223
|
+
secondaryButtonOnClick: handleSecondaryButtonClick,
|
|
224
|
+
onDismiss: handleDismiss,
|
|
207
225
|
sizePx,
|
|
208
226
|
theme,
|
|
209
227
|
isMobile,
|
|
@@ -211,7 +229,7 @@ export const Modal: FC<ModalProps> = ({
|
|
|
211
229
|
|
|
212
230
|
if (isMobile) {
|
|
213
231
|
return (
|
|
214
|
-
<ActionSheet ref={actionSheetRef} onClose={
|
|
232
|
+
<ActionSheet ref={actionSheetRef} onClose={handleDismiss}>
|
|
215
233
|
<PanGestureHandler onHandlerStateChange={onHandlerStateChange}>
|
|
216
234
|
<View>
|
|
217
235
|
<View
|
|
@@ -237,7 +255,7 @@ export const Modal: FC<ModalProps> = ({
|
|
|
237
255
|
);
|
|
238
256
|
} else {
|
|
239
257
|
return (
|
|
240
|
-
<RNModal animationType="slide" transparent visible={visible} onRequestClose={
|
|
258
|
+
<RNModal animationType="slide" transparent visible={visible} onRequestClose={handleDismiss}>
|
|
241
259
|
<ModalContent {...modalContentProps}>{children}</ModalContent>
|
|
242
260
|
</RNModal>
|
|
243
261
|
);
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import {Picker} from "@react-native-picker/picker";
|
|
2
|
+
import React, {useCallback, useMemo, useState} from "react";
|
|
3
|
+
import {Modal, Platform, Text, TouchableOpacity, View} from "react-native";
|
|
4
|
+
|
|
5
|
+
import {FieldOption, SelectBadgeProps, SurfaceTheme, TextTheme} from "./Common";
|
|
6
|
+
import {Icon} from "./Icon";
|
|
7
|
+
import {useTheme} from "./Theme";
|
|
8
|
+
|
|
9
|
+
export const SelectBadge = ({
|
|
10
|
+
value,
|
|
11
|
+
status = "info",
|
|
12
|
+
secondary = false,
|
|
13
|
+
customBackgroundColor,
|
|
14
|
+
customTextColor,
|
|
15
|
+
customBorderColor,
|
|
16
|
+
disabled = false,
|
|
17
|
+
options,
|
|
18
|
+
onChange,
|
|
19
|
+
}: SelectBadgeProps): React.ReactElement => {
|
|
20
|
+
const {theme} = useTheme();
|
|
21
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
22
|
+
// Temporary state to manage value changes for ios picker
|
|
23
|
+
// Assures the badge display value persists when user scrolls through options
|
|
24
|
+
const [iosDisplayValue, setIosDisplayValue] = useState<string | undefined>(value);
|
|
25
|
+
|
|
26
|
+
const secondaryBorderColors = {
|
|
27
|
+
error: "#F39E9E",
|
|
28
|
+
warning: "#FCC58F",
|
|
29
|
+
info: "#8FC1D2",
|
|
30
|
+
success: "#7FD898",
|
|
31
|
+
neutral: "#AAAAAA",
|
|
32
|
+
custom: "#AAAAAA",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let borderWidth = 0;
|
|
36
|
+
if (secondary || status === "custom") borderWidth = 1;
|
|
37
|
+
|
|
38
|
+
let badgeColor: keyof TextTheme = "inverted";
|
|
39
|
+
|
|
40
|
+
if (secondary) {
|
|
41
|
+
if (status === "error") badgeColor = "error";
|
|
42
|
+
else if (status === "warning") badgeColor = "warning";
|
|
43
|
+
else if (status === "info") badgeColor = "secondaryDark";
|
|
44
|
+
else if (status === "success") badgeColor = "success";
|
|
45
|
+
else if (status === "neutral") badgeColor = "primary";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let badgeBgColor: keyof SurfaceTheme = "neutralDark";
|
|
49
|
+
|
|
50
|
+
if (status === "error") badgeBgColor = secondary ? "errorLight" : "error";
|
|
51
|
+
else if (status === "warning") badgeBgColor = secondary ? "warningLight" : "warning";
|
|
52
|
+
else if (status === "info") badgeBgColor = secondary ? "secondaryLight" : "secondaryDark";
|
|
53
|
+
else if (status === "success") badgeBgColor = secondary ? "successLight" : "success";
|
|
54
|
+
else if (status === "neutral") badgeBgColor = secondary ? "neutralLight" : "neutralDark";
|
|
55
|
+
|
|
56
|
+
const backgroundColor = status === "custom" ? customBackgroundColor : theme.surface[badgeBgColor];
|
|
57
|
+
const borderColor = status === "custom" ? customBorderColor : secondaryBorderColors[status];
|
|
58
|
+
const textColor = status === "custom" ? customTextColor : theme.text[badgeColor];
|
|
59
|
+
|
|
60
|
+
let leftOfChevronBorderColor = textColor;
|
|
61
|
+
if (status === "custom") leftOfChevronBorderColor = customBorderColor ?? textColor;
|
|
62
|
+
else if (secondary) leftOfChevronBorderColor = borderColor;
|
|
63
|
+
|
|
64
|
+
const findSelectedItem = useCallback(
|
|
65
|
+
(v: string | undefined | null): FieldOption | null => {
|
|
66
|
+
if (v !== undefined && v !== null) {
|
|
67
|
+
return options.find((opt) => opt.value === v) || null;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
},
|
|
71
|
+
[options]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const displayVal = useMemo(() => {
|
|
75
|
+
return findSelectedItem(value)?.label ?? "---";
|
|
76
|
+
}, [value, findSelectedItem]);
|
|
77
|
+
|
|
78
|
+
const handleOnChange = useCallback(
|
|
79
|
+
(val: string) => {
|
|
80
|
+
const selectedItem = findSelectedItem(val);
|
|
81
|
+
if (selectedItem) {
|
|
82
|
+
onChange(selectedItem.value);
|
|
83
|
+
}
|
|
84
|
+
setShowPicker(false);
|
|
85
|
+
},
|
|
86
|
+
[findSelectedItem, onChange]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const renderPickerItems = useCallback(() => {
|
|
90
|
+
return options?.map((item: any) => (
|
|
91
|
+
<Picker.Item key={item.key || item.label} label={item.label} value={item.value} />
|
|
92
|
+
));
|
|
93
|
+
}, [options]);
|
|
94
|
+
|
|
95
|
+
const renderIosPicker = useCallback(() => {
|
|
96
|
+
const handleValueChangeIos = (itemValue: string) => {
|
|
97
|
+
setIosDisplayValue(itemValue);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleSave = () => {
|
|
101
|
+
if (iosDisplayValue && !disabled) {
|
|
102
|
+
handleOnChange(iosDisplayValue);
|
|
103
|
+
} else {
|
|
104
|
+
setShowPicker(false);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleDismiss = () => {
|
|
109
|
+
setShowPicker(false);
|
|
110
|
+
setIosDisplayValue(value);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Modal
|
|
115
|
+
animationType="slide"
|
|
116
|
+
supportedOrientations={["portrait", "landscape"]}
|
|
117
|
+
transparent
|
|
118
|
+
visible={showPicker}
|
|
119
|
+
onRequestClose={handleDismiss}
|
|
120
|
+
>
|
|
121
|
+
<View style={{flex: 1, justifyContent: "flex-end"}}>
|
|
122
|
+
<TouchableOpacity
|
|
123
|
+
accessibilityHint="Closes the picker modal"
|
|
124
|
+
accessibilityLabel="Dismiss picker modal"
|
|
125
|
+
activeOpacity={1}
|
|
126
|
+
style={{flex: 1}}
|
|
127
|
+
onPress={handleDismiss}
|
|
128
|
+
/>
|
|
129
|
+
<View
|
|
130
|
+
style={{
|
|
131
|
+
backgroundColor: theme.surface.neutralLight,
|
|
132
|
+
borderTopWidth: 1,
|
|
133
|
+
borderTopColor: theme.border.default,
|
|
134
|
+
height: 215,
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<View
|
|
138
|
+
style={{
|
|
139
|
+
width: "100%",
|
|
140
|
+
height: 45,
|
|
141
|
+
backgroundColor: "#f8f8f8",
|
|
142
|
+
alignItems: "center",
|
|
143
|
+
justifyContent: "center",
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<TouchableOpacity
|
|
147
|
+
accessibilityHint="Saves the selected value"
|
|
148
|
+
accessibilityLabel="Save selected value"
|
|
149
|
+
aria-role="button"
|
|
150
|
+
hitSlop={{top: 4, right: 4, bottom: 4, left: 4}}
|
|
151
|
+
style={{
|
|
152
|
+
alignSelf: "flex-end",
|
|
153
|
+
paddingRight: 12,
|
|
154
|
+
}}
|
|
155
|
+
onPress={handleSave}
|
|
156
|
+
>
|
|
157
|
+
<View>
|
|
158
|
+
<Text
|
|
159
|
+
style={{
|
|
160
|
+
color: "#007aff",
|
|
161
|
+
fontWeight: "600",
|
|
162
|
+
fontSize: 17,
|
|
163
|
+
paddingTop: 1,
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
Save
|
|
167
|
+
</Text>
|
|
168
|
+
</View>
|
|
169
|
+
</TouchableOpacity>
|
|
170
|
+
</View>
|
|
171
|
+
<Picker
|
|
172
|
+
enabled={!disabled}
|
|
173
|
+
selectedValue={iosDisplayValue}
|
|
174
|
+
onValueChange={handleValueChangeIos}
|
|
175
|
+
>
|
|
176
|
+
{renderPickerItems()}
|
|
177
|
+
</Picker>
|
|
178
|
+
</View>
|
|
179
|
+
</View>
|
|
180
|
+
</Modal>
|
|
181
|
+
);
|
|
182
|
+
}, [showPicker, iosDisplayValue, disabled, theme, value, handleOnChange, renderPickerItems]);
|
|
183
|
+
|
|
184
|
+
const renderPicker = useCallback(() => {
|
|
185
|
+
return (
|
|
186
|
+
<Picker
|
|
187
|
+
enabled={!disabled}
|
|
188
|
+
selectedValue={findSelectedItem(value)?.value ?? undefined}
|
|
189
|
+
style={[
|
|
190
|
+
{
|
|
191
|
+
position: "absolute",
|
|
192
|
+
width: "100%",
|
|
193
|
+
height: "100%",
|
|
194
|
+
color: "transparent",
|
|
195
|
+
opacity: 0,
|
|
196
|
+
},
|
|
197
|
+
// Android headless picker: transparent overlay to capture touches without visible UI.
|
|
198
|
+
Platform.OS !== "web" && {backgroundColor: "transparent"},
|
|
199
|
+
]}
|
|
200
|
+
onValueChange={handleOnChange}
|
|
201
|
+
>
|
|
202
|
+
{renderPickerItems()}
|
|
203
|
+
</Picker>
|
|
204
|
+
);
|
|
205
|
+
}, [disabled, findSelectedItem, value, handleOnChange, renderPickerItems]);
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<View style={{alignItems: "flex-start", opacity: disabled ? 0.5 : 1}}>
|
|
209
|
+
<TouchableOpacity
|
|
210
|
+
accessibilityHint="Opens the options picker"
|
|
211
|
+
accessibilityLabel="Open select badge options"
|
|
212
|
+
aria-role="button"
|
|
213
|
+
disabled={disabled}
|
|
214
|
+
onPress={() => setShowPicker(!showPicker)}
|
|
215
|
+
>
|
|
216
|
+
<View
|
|
217
|
+
style={{
|
|
218
|
+
display: "flex",
|
|
219
|
+
flexDirection: "row",
|
|
220
|
+
alignItems: "center",
|
|
221
|
+
height: 20,
|
|
222
|
+
width: "auto",
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
<View
|
|
226
|
+
style={{
|
|
227
|
+
justifyContent: "center",
|
|
228
|
+
alignItems: "center",
|
|
229
|
+
paddingHorizontal: theme.spacing.sm,
|
|
230
|
+
flexDirection: "row",
|
|
231
|
+
borderTopLeftRadius: 4,
|
|
232
|
+
borderBottomLeftRadius: 4,
|
|
233
|
+
backgroundColor,
|
|
234
|
+
height: 20,
|
|
235
|
+
width: "auto",
|
|
236
|
+
borderWidth,
|
|
237
|
+
borderColor,
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
<Text
|
|
241
|
+
style={{
|
|
242
|
+
color: textColor,
|
|
243
|
+
fontSize: 10,
|
|
244
|
+
fontWeight: "700",
|
|
245
|
+
fontFamily: "text",
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{displayVal}
|
|
249
|
+
</Text>
|
|
250
|
+
</View>
|
|
251
|
+
<View
|
|
252
|
+
style={{
|
|
253
|
+
justifyContent: "center",
|
|
254
|
+
alignItems: "center",
|
|
255
|
+
paddingVertical: 1,
|
|
256
|
+
paddingHorizontal: theme.spacing.xs,
|
|
257
|
+
flexDirection: "row",
|
|
258
|
+
borderTopRightRadius: 4,
|
|
259
|
+
borderBottomRightRadius: 4,
|
|
260
|
+
backgroundColor,
|
|
261
|
+
height: 20,
|
|
262
|
+
width: "auto",
|
|
263
|
+
borderWidth,
|
|
264
|
+
borderLeftWidth: 1,
|
|
265
|
+
borderColor: leftOfChevronBorderColor,
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
<Icon
|
|
269
|
+
color={textColor as any}
|
|
270
|
+
iconName={showPicker ? "chevron-up" : "chevron-down"}
|
|
271
|
+
size="sm"
|
|
272
|
+
/>
|
|
273
|
+
</View>
|
|
274
|
+
</View>
|
|
275
|
+
</TouchableOpacity>
|
|
276
|
+
{Platform.OS === "ios" ? renderIosPicker() : renderPicker()}
|
|
277
|
+
</View>
|
|
278
|
+
);
|
|
279
|
+
};
|