@wq/material-native 2.0.0-alpha.0 → 2.0.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.
Files changed (40) hide show
  1. package/babel.config.cjs +1 -1
  2. package/jest.config.js +1 -5
  3. package/package.json +16 -17
  4. package/src/App.js +23 -5
  5. package/src/components/{ExpansionPanel.js → Accordion.js} +2 -2
  6. package/src/components/BottomNavigation.js +3 -0
  7. package/src/components/BottomNavigationAction.js +3 -0
  8. package/src/components/CheckboxButton.js +25 -2
  9. package/src/components/Fab.js +0 -1
  10. package/src/components/IconButton.js +1 -1
  11. package/src/components/Img.js +102 -0
  12. package/src/components/ListItem.js +17 -1
  13. package/src/components/NavMenuFixed.js +3 -0
  14. package/src/components/NavMenuPopup.js +3 -0
  15. package/src/components/Popup.js +18 -2
  16. package/src/components/RadioButton.js +25 -2
  17. package/src/components/SidePanel.js +93 -2
  18. package/src/components/Switch.js +17 -2
  19. package/src/components/TabGroup.js +38 -0
  20. package/src/components/TabItem.js +16 -0
  21. package/src/components/TableCell.js +23 -1
  22. package/src/components/index.js +17 -10
  23. package/src/icons.js +16 -1
  24. package/src/inputs/Checkbox.js +22 -13
  25. package/src/inputs/DateTime.js +8 -5
  26. package/src/inputs/File.js +175 -2
  27. package/src/inputs/FileArray.js +12 -0
  28. package/src/inputs/HelperText.js +28 -2
  29. package/src/inputs/Hidden.js +5 -4
  30. package/src/inputs/Image.js +5 -2
  31. package/src/inputs/Input.js +17 -11
  32. package/src/inputs/Radio.js +31 -2
  33. package/src/inputs/Select.js +22 -16
  34. package/src/inputs/Toggle.js +37 -2
  35. package/src/inputs/index.js +8 -0
  36. package/setup-jest.js +0 -5
  37. package/src/components/FileArray.js +0 -3
  38. /package/src/{components → inputs}/Fieldset.js +0 -0
  39. /package/src/{components → inputs}/FieldsetArray.js +0 -0
  40. /package/src/{components → inputs}/FlatFieldset.js +0 -0
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",
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",
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
  }
package/src/App.js CHANGED
@@ -1,11 +1,20 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { App as DefaultApp, usePlugin } from "@wq/react";
3
3
  import {
4
- DefaultTheme,
5
- DarkTheme,
4
+ MD2LightTheme,
5
+ MD3LightTheme,
6
+ MD2DarkTheme,
7
+ MD3DarkTheme,
6
8
  Provider as PaperProvider,
7
9
  } from "react-native-paper";
8
10
 
11
+ const THEMES = {
12
+ "light-2": MD2LightTheme,
13
+ "light-3": MD3LightTheme,
14
+ "dark-2": MD2DarkTheme,
15
+ "dark-3": MD3DarkTheme,
16
+ };
17
+
9
18
  export default function App() {
10
19
  const { theme: configTheme } = usePlugin("material").config,
11
20
  theme = useMemo(() => createTheme(configTheme), [configTheme]),
@@ -26,9 +35,18 @@ export default function App() {
26
35
  );
27
36
  }
28
37
 
29
- function createTheme({ type, primary, secondary, background }) {
38
+ function createTheme({
39
+ type = "light",
40
+ version = 3,
41
+ primary,
42
+ secondary,
43
+ background,
44
+ }) {
30
45
  const colors = {},
31
- base = type === "dark" ? DarkTheme : DefaultTheme;
46
+ base = THEMES[`${type}-${version}`];
47
+ if (!base) {
48
+ console.warn(`Unknown base theme type=${type} version=${version}`);
49
+ }
32
50
  if (primary) {
33
51
  colors.primary = primary;
34
52
  }
@@ -41,7 +59,7 @@ function createTheme({ type, primary, secondary, background }) {
41
59
  return {
42
60
  ...base,
43
61
  colors: {
44
- ...base.colors,
62
+ ...(base || {}).colors,
45
63
  ...colors,
46
64
  },
47
65
  };
@@ -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,
@@ -18,7 +18,7 @@ export default function IconButton({ icon, onClick, onPress, color, ...rest }) {
18
18
  <PaperIconButton
19
19
  icon={Icon}
20
20
  onPress={onPress}
21
- color={color}
21
+ iconColor={color}
22
22
  {...rest}
23
23
  />
24
24
  );
@@ -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 };