@wq/material-native 2.0.0-alpha.0 → 2.0.0-alpha.2

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/babel.config.cjs CHANGED
@@ -2,7 +2,7 @@ module.exports = {
2
2
  env: {
3
3
  test: {
4
4
  plugins: [
5
- ["@babel/plugin-proposal-private-methods", { loose: true }],
5
+ ["@babel/plugin-transform-private-methods", { loose: true }],
6
6
  ],
7
7
  presets: ["module:metro-react-native-babel-preset"],
8
8
  },
package/jest.config.js CHANGED
@@ -1,9 +1,5 @@
1
1
  export default {
2
- preset: "react-native",
3
- setupFiles: [
4
- "./node_modules/react-native-gesture-handler/jestSetup.js",
5
- "./setup-jest.js",
6
- ],
2
+ preset: "jest-expo",
7
3
  testMatch: ["**/__tests__/**/*.js?(x)"],
8
4
  transformIgnorePatterns: [],
9
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wq/material-native",
3
- "version": "2.0.0-alpha.0",
3
+ "version": "2.0.0-alpha.2",
4
4
  "description": "React Native and Expo bindings for @wq/material",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -28,31 +28,30 @@
28
28
  },
29
29
  "homepage": "https://wq.io/@wq/material",
30
30
  "dependencies": {
31
- "@wq/react": "^2.0.0-alpha.0",
32
- "date-fns": "^2.29.3"
31
+ "@wq/react": "^2.0.0-alpha.2",
32
+ "date-fns": "^2.30.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@react-native-async-storage/async-storage": "*",
36
- "@react-native-community/datetimepicker": "*",
37
- "@react-native-community/masked-view": "*",
38
36
  "@react-native-community/netinfo": "*",
39
- "@react-navigation/native": "*",
40
37
  "@react-navigation/stack": "*",
38
+ "expo-application": "*",
39
+ "expo-document-picker": "*",
40
+ "expo-image-picker": "*",
41
41
  "react-native-modal-datetime-picker": "*",
42
42
  "react-native-paper": "*",
43
43
  "react-native-picker-select": "*"
44
44
  },
45
45
  "devDependencies": {
46
- "@react-native-async-storage/async-storage": "^1.17.11",
47
- "@react-native-community/datetimepicker": "^6.7.1",
48
- "@react-native-community/masked-view": "^0.1.11",
49
- "@react-native-community/netinfo": "^9.3.7",
50
- "@react-navigation/native": "^6.1.2",
51
- "@react-navigation/stack": "^6.3.11",
52
- "@wq/model": "^2.0.0-alpha.0",
53
- "@wq/store": "^2.0.0-alpha.0",
54
- "react-native-modal-datetime-picker": "^14.0.1",
55
- "react-native-paper": "^5.1.3",
56
- "react-native-picker-select": "^8.0.4"
46
+ "@react-native-async-storage/async-storage": "^1.19.1",
47
+ "@react-native-community/netinfo": "^9.4.1",
48
+ "@react-navigation/stack": "^6.3.17",
49
+ "expo-application": "^5.3.0",
50
+ "expo-document-picker": "^11.5.4",
51
+ "expo-image-picker": "^14.3.2",
52
+ "jest-expo": "^49.0.0",
53
+ "react-native-modal-datetime-picker": "^17.0.0",
54
+ "react-native-paper": "^5.10.1",
55
+ "react-native-picker-select": "github:Expensify/react-native-picker-select#eae0585"
57
56
  }
58
57
  }
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { List } from "react-native-paper";
3
3
  import PropTypes from "prop-types";
4
4
 
5
- export default function ExpansionPanel({ summary, children, open, onToggle }) {
5
+ export default function Accordion({ summary, children, open, onToggle }) {
6
6
  let handleToggle;
7
7
  if (onToggle) {
8
8
  handleToggle = () => onToggle(!open);
@@ -14,7 +14,7 @@ export default function ExpansionPanel({ summary, children, open, onToggle }) {
14
14
  );
15
15
  }
16
16
 
17
- ExpansionPanel.propTypes = {
17
+ Accordion.propTypes = {
18
18
  summary: PropTypes.node,
19
19
  children: PropTypes.node,
20
20
  open: PropTypes.bool,
@@ -0,0 +1,3 @@
1
+ export default function BottomNavigation() {
2
+ return null; //FIXME
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function BottomNavigationAction() {
2
+ return null; //FIXME
3
+ }
@@ -1,3 +1,26 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React from "react";
2
+ import { Checkbox as PaperCheckbox, useTheme } from "react-native-paper";
3
+ import PropTypes from "prop-types";
4
+
5
+ const STATUSES = {
6
+ true: "checked",
7
+ false: "unchecked",
8
+ undefined: "indeterminate",
9
+ null: "indeterminate",
10
+ };
11
+
12
+ export default function Checkbox({ checked, color, ...props }) {
13
+ const status = STATUSES[checked],
14
+ theme = useTheme();
15
+ if (color === "primary") {
16
+ color = theme.colors.primary;
17
+ } else if (color === "secondary") {
18
+ color = theme.colors.accent;
19
+ }
20
+ return <PaperCheckbox status={status} {...props} />;
3
21
  }
22
+
23
+ Checkbox.propTypes = {
24
+ checked: PropTypes.bool,
25
+ color: PropTypes.string,
26
+ };
@@ -11,7 +11,6 @@ export default function Fab({ icon, to, ...rest }) {
11
11
  <FAB
12
12
  onPress={onPress}
13
13
  icon={Icon}
14
- color="white"
15
14
  style={{
16
15
  position: "absolute",
17
16
  margin: 16,
@@ -1,3 +1,12 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React from "react";
2
+ import { List } from "react-native-paper";
3
+ import FieldsetArray from "./FieldsetArray.js";
4
+
5
+ export default function FileArray(props) {
6
+ return (
7
+ <>
8
+ {props.label && <List.Subheader>{props.label}</List.Subheader>}
9
+ <FieldsetArray {...props} />
10
+ </>
11
+ );
3
12
  }
@@ -0,0 +1,102 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Image, TouchableOpacity, Dimensions } from "react-native";
3
+ import PropTypes from "prop-types";
4
+
5
+ export default function Img({ src, onPress, onClick, style, ...props }) {
6
+ const [source, setSource] = useState(src ? { uri: src } : null);
7
+ useEffect(() => {
8
+ if (!src) {
9
+ return;
10
+ }
11
+ if (typeof src !== "string") {
12
+ setSource(src);
13
+ return;
14
+ }
15
+ Image.getSize(src, (width, height) => {
16
+ setSource({ uri: src, width, height });
17
+ });
18
+ }, [src]);
19
+
20
+ let resizeMode;
21
+ if (
22
+ style &&
23
+ typeof style.maxHeight === "number" &&
24
+ typeof style.maxWidth === "string" &&
25
+ style.maxWidth.endsWith("vw")
26
+ ) {
27
+ resizeMode = "contain";
28
+ const ratio =
29
+ source && source.width && source.height
30
+ ? source.width / source.height
31
+ : 1,
32
+ maxWidth =
33
+ (+style.maxWidth.replace(/vw/g, "") / 100) *
34
+ Dimensions.get("window").width;
35
+ style = {
36
+ ...style,
37
+ maxHeight: undefined,
38
+ maxWidth: undefined,
39
+ height: style.maxHeight,
40
+ width: Math.min(ratio * style.maxHeight, maxWidth),
41
+ };
42
+ } else if (
43
+ style &&
44
+ typeof style.maxHeight === "string" &&
45
+ typeof style.maxWidth === "string"
46
+ ) {
47
+ resizeMode = "contain";
48
+ style = {
49
+ ...style,
50
+ maxHeight: undefined,
51
+ maxWidth: undefined,
52
+ height: style.maxHeight.replace("vh", "%"),
53
+ width: style.maxWidth.replace("vw", "%"),
54
+ };
55
+ }
56
+
57
+ const hasSize =
58
+ (source && (source === src || (source.width && source.height))) ||
59
+ props.width ||
60
+ props.height ||
61
+ (style && style.width) ||
62
+ (style && style.height);
63
+ if (src && !hasSize) {
64
+ return null;
65
+ } else if (onPress || onClick) {
66
+ return (
67
+ <TouchableOpacity
68
+ onPress={onPress || onClick}
69
+ style={{
70
+ ...style,
71
+ alignItems: "center",
72
+ justifyContent: "center",
73
+ }}
74
+ >
75
+ <Image
76
+ resizeMode={resizeMode}
77
+ style={style}
78
+ source={source}
79
+ {...props}
80
+ />
81
+ </TouchableOpacity>
82
+ );
83
+ } else {
84
+ return (
85
+ <Image
86
+ resizeMode={resizeMode}
87
+ style={style}
88
+ source={source}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+ }
94
+
95
+ Img.propTypes = {
96
+ src: PropTypes.oneOfType(PropTypes.string, PropTypes.number),
97
+ width: PropTypes.number,
98
+ height: PropTypes.number,
99
+ onPress: PropTypes.func,
100
+ onClick: PropTypes.func,
101
+ style: PropTypes.object,
102
+ };
@@ -3,13 +3,26 @@ import { useIcon } from "@wq/react";
3
3
  import { List } from "react-native-paper";
4
4
  import PropTypes from "prop-types";
5
5
 
6
- export default function ListItem({ children, description, icon, ...rest }) {
6
+ export default function ListItem({
7
+ children,
8
+ description,
9
+ icon,
10
+ dense,
11
+ style,
12
+ onClick,
13
+ ...rest
14
+ }) {
7
15
  const Icon = useIcon(icon);
8
16
  return (
9
17
  <List.Item
10
18
  title={children}
11
19
  description={description}
12
20
  left={Icon ? (props) => <List.Icon icon={Icon} {...props} /> : null}
21
+ onPress={onClick}
22
+ style={{
23
+ ...(dense && { paddingTop: 4, paddingBottom: 4 }),
24
+ ...style,
25
+ }}
13
26
  {...rest}
14
27
  />
15
28
  );
@@ -19,4 +32,7 @@ ListItem.propTypes = {
19
32
  children: PropTypes.node,
20
33
  description: PropTypes.node,
21
34
  icon: PropTypes.string,
35
+ dense: PropTypes.bool,
36
+ style: PropTypes.object,
37
+ onClick: PropTypes.func,
22
38
  };
@@ -0,0 +1,3 @@
1
+ export default function NavMenuFixed() {
2
+ return null; //FIXME
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function NavMenuPopup() {
2
+ return null; // FIXME
3
+ }
@@ -1,3 +1,19 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React from "react";
2
+ import { Modal, Portal } from "react-native-paper";
3
+ import PropTypes from "prop-types";
4
+
5
+ export default function Popup({ open, onClose, children, ...rest }) {
6
+ return (
7
+ <Portal>
8
+ <Modal visible={open} onDismiss={onClose} {...rest}>
9
+ {children}
10
+ </Modal>
11
+ </Portal>
12
+ );
3
13
  }
14
+
15
+ Popup.propTypes = {
16
+ open: PropTypes.bool,
17
+ onClose: PropTypes.func,
18
+ children: PropTypes.node,
19
+ };
@@ -1,3 +1,26 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React from "react";
2
+ import { RadioButton as PaperRadioButton, useTheme } from "react-native-paper";
3
+ import PropTypes from "prop-types";
4
+
5
+ const STATUSES = {
6
+ true: "checked",
7
+ false: "unchecked",
8
+ undefined: "unchecked",
9
+ null: "unchecked",
10
+ };
11
+
12
+ export default function RadioButton({ checked, color, ...rest }) {
13
+ const status = STATUSES[checked],
14
+ theme = useTheme();
15
+ if (color === "primary") {
16
+ color = theme.colors.primary;
17
+ } else if (color === "secondary") {
18
+ color = theme.colors.accent;
19
+ }
20
+ return <PaperRadioButton status={status} {...rest} />;
3
21
  }
22
+
23
+ RadioButton.propTypes = {
24
+ checked: PropTypes.bool,
25
+ color: PropTypes.string,
26
+ };
@@ -1,3 +1,94 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React, { useState, useEffect } from "react";
2
+ import { useComponents } from "@wq/react";
3
+ import { View } from "react-native";
4
+ import { useTheme } from "react-native-paper";
5
+ import { useMinWidth } from "../hooks.js";
6
+ import PropTypes from "prop-types";
7
+
8
+ export default function SidePanel({
9
+ anchor: anchorSpec = "top-left",
10
+ children,
11
+ compactChildren,
12
+ onChange,
13
+ }) {
14
+ const mobile = !useMinWidth(900),
15
+ theme = useTheme(),
16
+ anchor = anchorSpec.endsWith("right") ? "right" : "left",
17
+ vAnchor = anchorSpec.startsWith("bottom") ? "bottom" : "top",
18
+ [open, setOpen] = useState(!mobile),
19
+ { IconButton } = useComponents(),
20
+ drawerStyle = {},
21
+ iconStyle = {};
22
+
23
+ drawerStyle.zIndex = 1000;
24
+ drawerStyle.width = open ? 280 : 50;
25
+ drawerStyle.borderColor = "rgba(0, 0, 0, 0.12)";
26
+
27
+ if (mobile && !open) {
28
+ drawerStyle.backgroundColor = theme.colors.background;
29
+ drawerStyle.position = "absolute";
30
+ if (vAnchor === "bottom") {
31
+ drawerStyle.bottom = 0;
32
+ drawerStyle.borderTopWidth = 1;
33
+ } else {
34
+ drawerStyle.top = 0;
35
+ drawerStyle.borderBottomWidth = 1;
36
+ }
37
+ if (anchor === "right") {
38
+ drawerStyle.right = 1;
39
+ } else {
40
+ drawerStyle.left = 1;
41
+ }
42
+ }
43
+
44
+ if (anchor === "right") {
45
+ iconStyle.transform = [{ rotate: "180deg" }];
46
+ drawerStyle.borderLeftWidth = 1;
47
+ } else {
48
+ drawerStyle.borderRightWidth = 1;
49
+ }
50
+
51
+ useEffect(() => {
52
+ if (onChange) {
53
+ onChange(open);
54
+ }
55
+ }, [open, onChange]);
56
+
57
+ return (
58
+ <View style={drawerStyle}>
59
+ {open ? (
60
+ <View
61
+ style={{
62
+ display: "flex",
63
+ flexDirection: "row",
64
+ position: "sticky",
65
+ top: 0,
66
+ zIndex: 100,
67
+ }}
68
+ >
69
+ {anchor === "left" && <View style={{ flex: 1 }} />}
70
+ <IconButton
71
+ icon="panel-close"
72
+ style={iconStyle}
73
+ onClick={() => setOpen(false)}
74
+ />
75
+ {anchor === "right" && <View style={{ flex: 1 }} />}
76
+ </View>
77
+ ) : (
78
+ <IconButton
79
+ icon="panel-open"
80
+ style={iconStyle}
81
+ onClick={() => setOpen(true)}
82
+ />
83
+ )}
84
+ {open ? children : compactChildren}
85
+ </View>
86
+ );
3
87
  }
88
+
89
+ SidePanel.propTypes = {
90
+ children: PropTypes.node,
91
+ compactChildren: PropTypes.node,
92
+ onChange: PropTypes.func,
93
+ anchor: PropTypes.string,
94
+ };
@@ -1,3 +1,18 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React from "react";
2
+ import { Switch as PaperSwitch, useTheme } from "react-native-paper";
3
+ import PropTypes from "prop-types";
4
+
5
+ export default function Switch({ checked: value, color, ...rest }) {
6
+ const theme = useTheme();
7
+ if (color === "primary") {
8
+ color = theme.colors.primary;
9
+ } else if (color === "secondary") {
10
+ color = theme.colors.accent;
11
+ }
12
+ return <PaperSwitch value={value} {...rest} />;
3
13
  }
14
+
15
+ Switch.propTypes = {
16
+ checked: PropTypes.bool,
17
+ color: PropTypes.string,
18
+ };
@@ -0,0 +1,38 @@
1
+ import React, { useState } from "react";
2
+ import { useIconComponents } from "@wq/react";
3
+ import { View } from "react-native";
4
+ import { SegmentedButtons } from "react-native-paper";
5
+ import PropTypes from "prop-types";
6
+
7
+ export default function TabGroup({ children, style, ...rest }) {
8
+ const tabs = React.Children.toArray(children),
9
+ [value, setValue] = useState(tabs[0].props.value),
10
+ activeTab = tabs.find((tab) => tab.props.value === value),
11
+ handleChange = (tab) => setValue(tab),
12
+ icons = useIconComponents(),
13
+ buttons = tabs.map((tab) => ({
14
+ value: tab.props.value,
15
+ label: tab.props.label,
16
+ icon: icons[tab.props.icon],
17
+ }));
18
+
19
+ return (
20
+ <View style={{ flex: 1, display: "flex", flexDirection: "column" }}>
21
+ <SegmentedButtons
22
+ value={value}
23
+ onValueChange={handleChange}
24
+ buttons={buttons}
25
+ style={{ padding: 8, ...style }}
26
+ {...rest}
27
+ />
28
+ <View style={{ flex: 1, display: "flex", flexDirection: "column" }}>
29
+ {activeTab && activeTab.props.children}
30
+ </View>
31
+ </View>
32
+ );
33
+ }
34
+
35
+ TabGroup.propTypes = {
36
+ children: PropTypes.node,
37
+ style: PropTypes.object,
38
+ };
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default function TabItem({ value, icon, label, children }) {
5
+ /* eslint no-unused-vars: off */
6
+
7
+ // All rendering handled by TabGroup
8
+ return null;
9
+ }
10
+
11
+ TabItem.propTypes = {
12
+ value: PropTypes.string,
13
+ icon: PropTypes.string,
14
+ label: PropTypes.string,
15
+ children: PropTypes.node,
16
+ };
@@ -1,3 +1,25 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
1
3
  import { DataTable } from "react-native-paper";
4
+ import PropTypes from "prop-types";
2
5
 
3
- export default DataTable.Cell;
6
+ export default function TableCell({ interactive, children, ...rest }) {
7
+ const Cell = interactive ? InteractiveCell : DataTable.Cell;
8
+ return <Cell {...rest}>{children}</Cell>;
9
+ }
10
+
11
+ function InteractiveCell(props) {
12
+ return (
13
+ <View
14
+ style={{
15
+ flex: 1,
16
+ flexDirection: "row",
17
+ alignItems: "center",
18
+ ...props.style,
19
+ }}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ TableCell.propTypes = { interactive: PropTypes.bool, children: PropTypes.node };
@@ -1,5 +1,7 @@
1
1
  import Container from "./Container.js";
2
2
  import Header from "./Header.js";
3
+ import NavMenuPopup from "./NavMenuPopup.js";
4
+ import NavMenuFixed from "./NavMenuFixed.js";
3
5
  import Main from "./Main.js";
4
6
  import Footer from "./Footer.js";
5
7
  import FooterContent from "./FooterContent.js";
@@ -7,6 +9,7 @@ import View from "./View.js";
7
9
  import ScrollView from "./ScrollView.js";
8
10
  import HorizontalView from "./HorizontalView.js";
9
11
  import Text from "./Text.js";
12
+ import Img from "./Img.js";
10
13
  import Typography from "./Typography.js";
11
14
  import FormatJson from "./FormatJson.js";
12
15
 
@@ -16,6 +19,10 @@ import ButtonLink from "./ButtonLink.js";
16
19
  import HomeLink from "./HomeLink.js";
17
20
  import IconButton from "./IconButton.js";
18
21
  import Fab from "./Fab.js";
22
+ import TabGroup from "./TabGroup.js";
23
+ import TabItem from "./TabItem.js";
24
+ import BottomNavigation from "./BottomNavigation.js";
25
+ import BottomNavigationAction from "./BottomNavigationAction.js";
19
26
  import Chip from "./Chip.js";
20
27
  import Switch from "./Switch.js";
21
28
  import CheckboxButton from "./CheckboxButton.js";
@@ -25,7 +32,7 @@ import List from "./List.js";
25
32
  import ListItem from "./ListItem.js";
26
33
  import ListItemLink from "./ListItemLink.js";
27
34
  import ListSubheader from "./ListSubheader.js";
28
- import ExpansionPanel from "./ExpansionPanel.js";
35
+ import Accordion from "./Accordion.js";
29
36
  import ExpandableListItem from "./ExpandableListItem.js";
30
37
  import Divider from "./Divider.js";
31
38
 
@@ -58,6 +65,8 @@ import Pagination from "./Pagination.js";
58
65
  export {
59
66
  Container,
60
67
  Header,
68
+ NavMenuPopup,
69
+ NavMenuFixed,
61
70
  Main,
62
71
  Footer,
63
72
  FooterContent,
@@ -65,6 +74,7 @@ export {
65
74
  ScrollView,
66
75
  HorizontalView,
67
76
  Text,
77
+ Img,
68
78
  Typography,
69
79
  FormatJson,
70
80
  };
@@ -76,6 +86,10 @@ export {
76
86
  HomeLink,
77
87
  IconButton,
78
88
  Fab,
89
+ TabGroup,
90
+ TabItem,
91
+ BottomNavigation,
92
+ BottomNavigationAction,
79
93
  Chip,
80
94
  Switch,
81
95
  CheckboxButton,
@@ -87,7 +101,8 @@ export {
87
101
  ListItem,
88
102
  ListItemLink,
89
103
  ListSubheader,
90
- ExpansionPanel,
104
+ Accordion,
105
+ Accordion as ExpansionPanel,
91
106
  ExpandableListItem,
92
107
  Divider,
93
108
  };
package/src/icons.js CHANGED
@@ -4,6 +4,21 @@ export const Add = "plus",
4
4
  Success = "check",
5
5
  Error = "alert-circle",
6
6
  Pending = "sync",
7
+ Close = "close",
8
+ Expand = "chevron-down",
9
+ Collapse = "chevron-up",
10
+ PanelOpen = "chevron-right",
11
+ PanelClose = "chevron-left",
7
12
  GpsStart = "crosshairs-gps",
8
13
  GpsStop = "crosshairs-off",
9
- Search = "magnify";
14
+ Search = "magnify",
15
+ List = "view-list",
16
+ Detail = "information",
17
+ Map = "map",
18
+ Home = "home",
19
+ BreadcrumbSeparator = "chevron-right",
20
+ Menu = "menu",
21
+ Login = "login",
22
+ Logout = "logout",
23
+ Config = "cog",
24
+ External = "open-in-new";
@@ -2,9 +2,10 @@ import React from "react";
2
2
  import { useField } from "formik";
3
3
  import { Checkbox as PaperCheckbox, useTheme } from "react-native-paper";
4
4
  import { Text, View } from "react-native";
5
+ import HelperText from "./HelperText.js";
5
6
  import PropTypes from "prop-types";
6
7
 
7
- export default function Checkbox({ name, label }) {
8
+ export default function Checkbox({ name, label, hint }) {
8
9
  const theme = useTheme(),
9
10
  [, meta, helpers] = useField(name),
10
11
  { value } = meta,
@@ -15,19 +16,26 @@ export default function Checkbox({ name, label }) {
15
16
  }
16
17
 
17
18
  return (
18
- <View
19
- style={{ flexDirection: "row", alignItems: "center", padding: 8 }}
20
- >
21
- <PaperCheckbox.Android
22
- status={value ? "checked" : "unchecked"}
23
- onPress={toggleChecked}
24
- />
25
- <Text
26
- style={{ color: theme.colors.text, fontSize: 16, flex: 1 }}
27
- onPress={toggleChecked}
19
+ <View>
20
+ <View
21
+ style={{
22
+ flexDirection: "row",
23
+ alignItems: "center",
24
+ padding: 8,
25
+ }}
28
26
  >
29
- {label}
30
- </Text>
27
+ <PaperCheckbox.Android
28
+ status={value ? "checked" : "unchecked"}
29
+ onPress={toggleChecked}
30
+ />
31
+ <Text
32
+ style={{ color: theme.colors.text, fontSize: 16, flex: 1 }}
33
+ onPress={toggleChecked}
34
+ >
35
+ {label}
36
+ </Text>
37
+ </View>
38
+ <HelperText name={name} hint={hint} />
31
39
  </View>
32
40
  );
33
41
  }
@@ -35,4 +43,5 @@ export default function Checkbox({ name, label }) {
35
43
  Checkbox.propTypes = {
36
44
  name: PropTypes.string,
37
45
  label: PropTypes.string,
46
+ hint: PropTypes.string,
38
47
  };
@@ -1,10 +1,11 @@
1
1
  import React, { useState } from "react";
2
2
  import DateTimePickerModal from "react-native-modal-datetime-picker";
3
- import { Text } from "react-native";
3
+ import { Text, View } from "react-native";
4
4
  import { TouchableRipple, TextInput } from "react-native-paper";
5
5
  import { useField } from "formik";
6
- import PropTypes from "prop-types";
7
6
  import { format, parse } from "./date-utils.js";
7
+ import HelperText from "./HelperText.js";
8
+ import PropTypes from "prop-types";
8
9
 
9
10
  const displayFormat = {
10
11
  date: (value) => parse.date(value).toLocaleDateString(),
@@ -12,7 +13,7 @@ const displayFormat = {
12
13
  datetime: (value) => parse.datetime(value).toLocaleString(),
13
14
  };
14
15
 
15
- export default function DateTime({ name, type, label }) {
16
+ export default function DateTime({ name, type, label, hint }) {
16
17
  type = type.toLowerCase();
17
18
 
18
19
  const [, meta, helpers] = useField(name),
@@ -33,7 +34,7 @@ export default function DateTime({ name, type, label }) {
33
34
  }
34
35
 
35
36
  return (
36
- <>
37
+ <View>
37
38
  <TextInput
38
39
  label={label}
39
40
  value={value ? displayFormat[type](value) : ""}
@@ -43,6 +44,7 @@ export default function DateTime({ name, type, label }) {
43
44
  </TouchableRipple>
44
45
  )}
45
46
  />
47
+ <HelperText name={name} hint={hint} />
46
48
  <DateTimePickerModal
47
49
  isVisible={show}
48
50
  mode={type}
@@ -50,7 +52,7 @@ export default function DateTime({ name, type, label }) {
50
52
  onConfirm={onConfirm}
51
53
  onCancel={hidePicker}
52
54
  />
53
- </>
55
+ </View>
54
56
  );
55
57
  }
56
58
 
@@ -58,4 +60,5 @@ DateTime.propTypes = {
58
60
  name: PropTypes.string,
59
61
  type: PropTypes.string,
60
62
  label: PropTypes.string,
63
+ hint: PropTypes.string,
61
64
  };
@@ -1,3 +1,176 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React, { useState } from "react";
2
+ import { useField } from "formik";
3
+ import { useComponents } from "@wq/react";
4
+ import { View, Text } from "react-native";
5
+ import { Button, List, Menu } from "react-native-paper";
6
+ import * as Application from "expo-application";
7
+ import * as DocumentPicker from "expo-document-picker";
8
+ import * as ImagePicker from "expo-image-picker";
9
+ import { format } from "./date-utils.js";
10
+ import HelperText from "./HelperText.js";
11
+ import PropTypes from "prop-types";
12
+
13
+ export default function File({ name, accept, capture, required, label, hint }) {
14
+ const [, { value }, { setValue }] = useField(name),
15
+ [imageOpen, setImageOpen] = useState(false),
16
+ [menuOpen, setMenuOpen] = useState(false),
17
+ { Img, Popup } = useComponents(),
18
+ acceptImage = accept && accept.startsWith("image/"),
19
+ showImage = () => setImageOpen(true),
20
+ hideImage = () => setImageOpen(false),
21
+ showMenu = () => setMenuOpen(true),
22
+ hideMenu = () => setMenuOpen(false);
23
+
24
+ async function handleFile(mode) {
25
+ const value = await mode(accept, capture);
26
+ if (value || mode === clear) {
27
+ setValue(value);
28
+ }
29
+ hideMenu();
30
+ }
31
+
32
+ const modes = capture
33
+ ? [takePhoto]
34
+ : acceptImage
35
+ ? [takePhoto, pickFile]
36
+ : [pickFile];
37
+
38
+ if (value && !required) {
39
+ modes.push(clear);
40
+ }
41
+
42
+ return (
43
+ <View>
44
+ {label && <List.Subheader>{label}</List.Subheader>}
45
+ <View
46
+ style={{
47
+ flexDirection: "row",
48
+ alignItems: "center",
49
+ padding: 8,
50
+ }}
51
+ >
52
+ {isImage(value) && (
53
+ <Img
54
+ onPress={showImage}
55
+ style={{ width: 64, height: 64 }}
56
+ src={typeof value === "string" ? value : value.uri}
57
+ />
58
+ )}
59
+ {value && !isImage(value) && <List.Icon icon="attachment" />}
60
+ <Text style={{ flex: 1, padding: 8 }}>
61
+ {value ? value.name : "No file selected"}
62
+ </Text>
63
+ <Menu
64
+ visible={menuOpen}
65
+ onDismiss={hideMenu}
66
+ anchor={
67
+ <Button
68
+ onPress={
69
+ modes.length > 1
70
+ ? showMenu
71
+ : () => handleFile(modes[0])
72
+ }
73
+ icon={value ? "pencil" : "plus"}
74
+ mode="contained-tonal"
75
+ >
76
+ {value
77
+ ? "Change"
78
+ : capture
79
+ ? "Add Photo"
80
+ : acceptImage
81
+ ? "Add Image"
82
+ : "Add File"}
83
+ </Button>
84
+ }
85
+ >
86
+ {modes.map((mode) => (
87
+ <Menu.Item
88
+ key={mode.icon}
89
+ onPress={() => handleFile(mode)}
90
+ leadingIcon={mode.icon}
91
+ title={mode.label}
92
+ />
93
+ ))}
94
+ </Menu>
95
+ </View>
96
+ <HelperText name={name} hint={hint} />
97
+ <Popup open={value && imageOpen} onClose={hideImage}>
98
+ <Img
99
+ onPress={hideImage}
100
+ resizeMode="contain"
101
+ style={{
102
+ width: "90%",
103
+ height: "90%",
104
+ margin: "5%",
105
+ }}
106
+ src={typeof value === "string" ? value : value && value.uri}
107
+ />
108
+ </Popup>
109
+ </View>
110
+ );
3
111
  }
112
+ File.propTypes = {
113
+ name: PropTypes.string,
114
+ accept: PropTypes.string,
115
+ capture: PropTypes.string,
116
+ required: PropTypes.bool,
117
+ label: PropTypes.string,
118
+ hint: PropTypes.string,
119
+ };
120
+
121
+ function isImage(value) {
122
+ if (typeof value === "string") {
123
+ return IMAGE_EXTENSIONS.some((ext) =>
124
+ value.toLowerCase().endsWith(ext)
125
+ );
126
+ } else {
127
+ return (
128
+ value && value.type && value.type.startsWith("image/") && value.uri
129
+ );
130
+ }
131
+ }
132
+
133
+ function generateName() {
134
+ const appName = Application.applicationName.replace(/ /, ""),
135
+ timestamp = new Date(),
136
+ date = format.date(timestamp).replace(/-/g, ""),
137
+ time = format.time(timestamp).replace(/:/g, "");
138
+ return `${appName}_${date}_${time}.jpg`;
139
+ }
140
+
141
+ async function pickFile(accept) {
142
+ const result = await DocumentPicker.getDocumentAsync({ type: accept });
143
+ if (result.assets && result.assets.length > 0) {
144
+ const { name, mimeType: type, uri } = result.assets[0];
145
+ return { name, type, uri };
146
+ }
147
+ }
148
+ pickFile.icon = "folder";
149
+ pickFile.label = "Pick File";
150
+
151
+ async function takePhoto(accept, capture) {
152
+ const options = {};
153
+ if (capture) {
154
+ options.cameraType =
155
+ capture === "user"
156
+ ? ImagePicker.CameraType.front
157
+ : ImagePicker.CameraType.back;
158
+ }
159
+ const result = await ImagePicker.launchCameraAsync(options);
160
+ if (result.assets && result.assets.length > 0) {
161
+ const name = generateName(),
162
+ type = "image/jpeg",
163
+ { uri } = result.assets[0];
164
+ return { name, type, uri };
165
+ }
166
+ }
167
+ takePhoto.icon = "camera";
168
+ takePhoto.label = "Take Photo";
169
+
170
+ function clear() {
171
+ return null;
172
+ }
173
+ clear.icon = "delete";
174
+ clear.label = "Remove";
175
+
176
+ const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif"];
@@ -1,3 +1,29 @@
1
- import { HelperText } from "react-native-paper";
1
+ import React from "react";
2
+ import { useFormikContext, getIn } from "formik";
3
+ import { HelperText as FormHelperText } from "react-native-paper";
4
+ import PropTypes from "prop-types";
2
5
 
3
- export default HelperText;
6
+ export default function HelperText({ name, hint }) {
7
+ const { errors, touched } = useFormikContext(),
8
+ error = getIn(errors, name),
9
+ showError = !!error && !!getIn(touched, name);
10
+
11
+ if (showError) {
12
+ hint = error;
13
+ }
14
+
15
+ if (!hint) {
16
+ return null;
17
+ }
18
+
19
+ return (
20
+ <FormHelperText type={showError ? "error" : "info"}>
21
+ {hint}
22
+ </FormHelperText>
23
+ );
24
+ }
25
+
26
+ HelperText.propTypes = {
27
+ name: PropTypes.string,
28
+ hint: PropTypes.string,
29
+ };
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { Field, ErrorMessage } from "formik";
3
- import { HelperText } from "react-native-paper";
2
+ import { Field } from "formik";
3
+ import HelperText from "./HelperText.js";
4
4
  import PropTypes from "prop-types";
5
5
 
6
6
  function Empty() {
@@ -8,15 +8,16 @@ function Empty() {
8
8
  }
9
9
 
10
10
  export default function Hidden(props) {
11
- const { name } = props;
11
+ const { name, hint } = props;
12
12
  return (
13
13
  <>
14
14
  <Field {...props} component={Empty} />
15
- <ErrorMessage type="error" component={HelperText} name={name} />
15
+ <HelperText name={name} hint={hint} />
16
16
  </>
17
17
  );
18
18
  }
19
19
 
20
20
  Hidden.propTypes = {
21
21
  name: PropTypes.string,
22
+ hint: PropTypes.string,
22
23
  };
@@ -1,3 +1,6 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React from "react";
2
+ import File from "./File.js";
3
+
4
+ export default function Image(props) {
5
+ return <File accept="image/*" {...props} />;
3
6
  }
@@ -1,7 +1,9 @@
1
1
  import React from "react";
2
+ import { View } from "react-native";
2
3
  import { TextInput } from "react-native-paper";
3
4
  import { useField } from "formik";
4
5
  import { useHtmlInput } from "@wq/react";
6
+ import HelperText from "./HelperText.js";
5
7
  import PropTypes from "prop-types";
6
8
 
7
9
  const keyboards = {
@@ -12,7 +14,7 @@ const keyboards = {
12
14
  };
13
15
 
14
16
  export default function Input(props) {
15
- const { name, type, label, style, min, max } = props,
17
+ const { name, type, label, hint, style, min, max } = props,
16
18
  { maxLength } = useHtmlInput(props),
17
19
  [, meta, helpers] = useField(name),
18
20
  { value } = meta,
@@ -49,16 +51,19 @@ export default function Input(props) {
49
51
  }
50
52
 
51
53
  return (
52
- <TextInput
53
- label={label}
54
- multiline={type === "text"}
55
- keyboardType={keyboards[type] || "default"}
56
- maxLength={maxLength}
57
- onChangeText={handleChange}
58
- onBlur={() => setTouched(true)}
59
- value={formatValue}
60
- style={style}
61
- />
54
+ <View style={{ flex: (style || {}).flex }}>
55
+ <TextInput
56
+ label={label}
57
+ multiline={type === "text"}
58
+ keyboardType={keyboards[type] || "default"}
59
+ maxLength={maxLength}
60
+ onChangeText={handleChange}
61
+ onBlur={() => setTouched(true)}
62
+ value={formatValue}
63
+ style={style}
64
+ />
65
+ <HelperText name={name} hint={hint} />
66
+ </View>
62
67
  );
63
68
  }
64
69
 
@@ -66,6 +71,7 @@ Input.propTypes = {
66
71
  name: PropTypes.string,
67
72
  type: PropTypes.string,
68
73
  label: PropTypes.string,
74
+ hint: PropTypes.string,
69
75
  style: PropTypes.object,
70
76
  min: PropTypes.number,
71
77
  max: PropTypes.number,
@@ -1,3 +1,32 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { RadioButton, List } from "react-native-paper";
4
+ import HelperText from "./HelperText.js";
5
+ import { useField } from "formik";
6
+ import PropTypes from "prop-types";
7
+
8
+ export default function Radio({ name, choices, label, hint }) {
9
+ const [, { value }, { setValue }] = useField(name);
10
+ return (
11
+ <View>
12
+ <List.Subheader>{label}</List.Subheader>
13
+ <RadioButton.Group onValueChange={setValue} value={value}>
14
+ {choices.map((choice) => (
15
+ <RadioButton.Item
16
+ key={choice.name}
17
+ value={choice.name}
18
+ label={choice.label}
19
+ />
20
+ ))}
21
+ </RadioButton.Group>
22
+ <HelperText name={name} hint={hint} />
23
+ </View>
24
+ );
3
25
  }
26
+
27
+ Radio.propTypes = {
28
+ name: PropTypes.string,
29
+ label: PropTypes.string,
30
+ hint: PropTypes.string,
31
+ choices: PropTypes.arrayOf(PropTypes.object),
32
+ };
@@ -1,10 +1,12 @@
1
1
  import React from "react";
2
2
  import RNPickerSelect from "react-native-picker-select";
3
+ import { View } from "react-native";
3
4
  import { TextInput, useTheme } from "react-native-paper";
4
5
  import { useField } from "formik";
6
+ import HelperText from "./HelperText.js";
5
7
  import PropTypes from "prop-types";
6
8
 
7
- export default function Select({ name, choices, label }) {
9
+ export default function Select({ name, choices, label, hint }) {
8
10
  const theme = useTheme(),
9
11
  [, meta, helpers] = useField(name),
10
12
  { value } = meta,
@@ -27,26 +29,30 @@ export default function Select({ name, choices, label }) {
27
29
  };
28
30
 
29
31
  return (
30
- <TextInput
31
- label={label}
32
- value={"-"}
33
- render={() => (
34
- <RNPickerSelect
35
- value={value}
36
- onValueChange={setValue}
37
- items={choices.map(({ name, label }) => ({
38
- value: name,
39
- label,
40
- }))}
41
- style={styles}
42
- />
43
- )}
44
- />
32
+ <View>
33
+ <TextInput
34
+ label={label}
35
+ value={"-"}
36
+ render={() => (
37
+ <RNPickerSelect
38
+ value={value}
39
+ onValueChange={setValue}
40
+ items={choices.map(({ name, label }) => ({
41
+ value: name,
42
+ label,
43
+ }))}
44
+ style={styles}
45
+ />
46
+ )}
47
+ />
48
+ <HelperText name={name} hint={hint} />
49
+ </View>
45
50
  );
46
51
  }
47
52
 
48
53
  Select.propTypes = {
49
54
  name: PropTypes.string,
50
55
  label: PropTypes.string,
56
+ hint: PropTypes.string,
51
57
  choices: PropTypes.arrayOf(PropTypes.object),
52
58
  };
@@ -1,3 +1,38 @@
1
- export default function NotImplemented() {
2
- return null; // FIXME
1
+ import React from "react";
2
+ import { View, Text } from "react-native";
3
+ import { ToggleButton, List } from "react-native-paper";
4
+ import HelperText from "./HelperText.js";
5
+ import { useField } from "formik";
6
+ import PropTypes from "prop-types";
7
+
8
+ export default function Toggle({ name, choices, label, hint, style }) {
9
+ const [, { value }, { setValue }] = useField(name);
10
+ return (
11
+ <View>
12
+ <List.Subheader>{label}</List.Subheader>
13
+ <ToggleButton.Row
14
+ onValueChange={setValue}
15
+ value={value}
16
+ style={{ paddingLeft: 16, paddingBottom: 8, ...style }}
17
+ >
18
+ {choices.map((choice) => (
19
+ <ToggleButton
20
+ key={choice.name}
21
+ value={choice.name}
22
+ icon={() => <Text>{choice.label}</Text>}
23
+ style={{ width: undefined, padding: 8 }}
24
+ />
25
+ ))}
26
+ </ToggleButton.Row>
27
+ <HelperText name={name} hint={hint} />
28
+ </View>
29
+ );
3
30
  }
31
+
32
+ Toggle.propTypes = {
33
+ name: PropTypes.string,
34
+ label: PropTypes.string,
35
+ hint: PropTypes.string,
36
+ choices: PropTypes.arrayOf(PropTypes.object),
37
+ style: PropTypes.object,
38
+ };
package/setup-jest.js DELETED
@@ -1,5 +0,0 @@
1
- import { NativeModules } from "react-native";
2
-
3
- if (!NativeModules.PlatformLocalStorage) {
4
- NativeModules.PlatformLocalStorage = {};
5
- }