@onehat/ui 0.3.292 → 0.3.295

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.3.292",
3
+ "version": "0.3.295",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -31,6 +31,7 @@
31
31
  "@hookform/resolvers": "^3.3.1",
32
32
  "@k-renwick/colour-mixer": "^1.2.1",
33
33
  "@onehat/data": "^1.21.0",
34
+ "@react-native-community/slider": "^4.5.2",
34
35
  "@reduxjs/toolkit": "^1.9.5",
35
36
  "inflector-js": "^1.0.1",
36
37
  "js-cookie": "^3.0.5",
@@ -19,80 +19,80 @@ const InputWithTooltip = withTooltip(Input);
19
19
 
20
20
  function NumberElement(props) {
21
21
  let {
22
- value,
23
- setValue,
24
- minValue,
25
- maxValue,
26
- autoSubmitDelay = UiGlobals.autoSubmitDelay,
27
- tooltip = null,
28
- isDisabled = false,
29
- testID,
30
- ...propsToPass
31
- } = props,
32
- styles = UiGlobals.styles,
33
- debouncedSetValueRef = useRef(),
34
- [localValue, setLocalValue] = useState(value),
35
- onInputKeyPress = (e) => {
36
- const key = e.nativeEvent.key; // e.key works on web, but not mobile; so use e.nativeEvent.key which works on both
37
- switch(key) {
38
- case 'ArrowDown':
39
- onDecrement();
40
- break;
41
- case 'ArrowUp':
42
- onIncrement();
43
- break;
44
- case 'Enter':
45
- debouncedSetValueRef.current?.cancel();
46
- setValue(value);
47
- break;
48
- case 'ArrowLeft':
49
- case 'ArrowRight':
50
- case 'Tab':
51
- case 'Backspace':
52
- return;
53
- default:
54
- }
55
- if (!key.match(/^[\-\d\.]*$/)) {
56
- e.preventDefault(); // kill anything that's not a number
57
- }
58
- },
59
- onChangeText = (value) => {
22
+ value,
23
+ setValue,
24
+ minValue,
25
+ maxValue,
26
+ autoSubmitDelay = UiGlobals.autoSubmitDelay,
27
+ tooltip = null,
28
+ isDisabled = false,
29
+ testID,
30
+ ...propsToPass
31
+ } = props,
32
+ styles = UiGlobals.styles,
33
+ debouncedSetValueRef = useRef(),
34
+ [localValue, setLocalValue] = useState(value),
35
+ onInputKeyPress = (e) => {
36
+ const key = e.nativeEvent.key; // e.key works on web, but not mobile; so use e.nativeEvent.key which works on both
37
+ switch(key) {
38
+ case 'ArrowDown':
39
+ onDecrement();
40
+ break;
41
+ case 'ArrowUp':
42
+ onIncrement();
43
+ break;
44
+ case 'Enter':
45
+ debouncedSetValueRef.current?.cancel();
46
+ setValue(value);
47
+ break;
48
+ case 'ArrowLeft':
49
+ case 'ArrowRight':
50
+ case 'Tab':
51
+ case 'Backspace':
52
+ return;
53
+ default:
54
+ }
55
+ if (!key.match(/^[\-\d\.]*$/)) {
56
+ e.preventDefault(); // kill anything that's not a number
57
+ }
58
+ },
59
+ onChangeText = (value) => {
60
60
 
61
- if (value === '') {
62
- value = null; // empty string makes value null
63
- } else if (value.match(/\.$/)) { // value ends with a decimal point
64
- // don't parseFloat, otherwise we'll lose the decimal point
65
- } else if (value.match(/0$/)) { // value ends with a zero
66
- // don't parseFloat, otherwise we'll lose the ability to do things like 1.03
67
- } else {
68
- value = parseFloat(value, 10);
69
- }
70
- setLocalValue(value);
71
- debouncedSetValueRef.current(value);
72
- },
73
- onDecrement = () => {
74
- let localValue = value;
75
- if (minValue && localValue === minValue) {
76
- return;
77
- }
78
- if (!localValue) {
79
- localValue = 0;
80
- }
81
- localValue = parseFloat(localValue, 10) -1;
82
- setValue(localValue);
83
- },
84
- onIncrement = () => {
85
- let localValue = value;
86
- if (maxValue && localValue === maxValue) {
87
- return;
88
- }
89
- if (!localValue) {
90
- localValue = 0;
91
- }
92
- localValue = parseFloat(localValue, 10) +1;
93
- setValue(localValue);
94
- };
95
-
61
+ if (value === '') {
62
+ value = null; // empty string makes value null
63
+ } else if (value.match(/\.$/)) { // value ends with a decimal point
64
+ // don't parseFloat, otherwise we'll lose the decimal point
65
+ } else if (value.match(/0$/)) { // value ends with a zero
66
+ // don't parseFloat, otherwise we'll lose the ability to do things like 1.03
67
+ } else {
68
+ value = parseFloat(value, 10);
69
+ }
70
+ setLocalValue(value);
71
+ debouncedSetValueRef.current(value);
72
+ },
73
+ onDecrement = () => {
74
+ let localValue = value;
75
+ if (minValue && localValue === minValue) {
76
+ return;
77
+ }
78
+ if (!localValue) {
79
+ localValue = 0;
80
+ }
81
+ localValue = parseFloat(localValue, 10) -1;
82
+ setValue(localValue);
83
+ },
84
+ onIncrement = () => {
85
+ let localValue = value;
86
+ if (maxValue && localValue === maxValue) {
87
+ return;
88
+ }
89
+ if (!localValue) {
90
+ localValue = 0;
91
+ }
92
+ localValue = parseFloat(localValue, 10) +1;
93
+ setValue(localValue);
94
+ };
95
+
96
96
  useEffect(() => {
97
97
  // Set up debounce fn
98
98
  // Have to do this because otherwise, lodash tries to create a debounced version of the fn from only this render
@@ -0,0 +1,210 @@
1
+ import React, { useState, useEffect, useRef, } from 'react';
2
+ import {
3
+ Row,
4
+ Text,
5
+ } from 'native-base';
6
+ import Input from './Input.js';
7
+ import Slider from '@react-native-community/slider'; // https://www.npmjs.com/package/@react-native-community/slider
8
+ import UiGlobals from '../../../UiGlobals.js';
9
+ import testProps from '../../../Functions/testProps.js';
10
+ import withComponent from '../../Hoc/withComponent.js';
11
+ import withTooltip from '../../Hoc/withTooltip.js';
12
+ import withValue from '../../Hoc/withValue.js';
13
+ import _ from 'lodash';
14
+
15
+ const FAKE_ZERO = 0.0000000001; // Slider doesn't like zero
16
+
17
+ const InputWithTooltip = withTooltip(Input);
18
+
19
+ const
20
+ SliderElement = (props) => {
21
+ let {
22
+ value = 0,
23
+ setValue,
24
+ minValue = 0,
25
+ maxValue = 100,
26
+ step = 10,
27
+ autoSubmitDelay = UiGlobals.autoSubmitDelay,
28
+ tooltip = null,
29
+ isDisabled = false,
30
+ testID,
31
+ ...propsToPass
32
+ } = props,
33
+ styles = UiGlobals.styles,
34
+ debouncedSetValueRef = useRef(),
35
+ [localValue, setLocalValue] = useState(value),
36
+ onInputKeyPress = (e) => {
37
+ const key = e.nativeEvent.key; // e.key works on web, but not mobile; so use e.nativeEvent.key which works on both
38
+ switch(key) {
39
+ case 'ArrowDown':
40
+ onDecrement();
41
+ break;
42
+ case 'ArrowUp':
43
+ onIncrement();
44
+ break;
45
+ case 'Enter':
46
+ debouncedSetValueRef.current?.cancel();
47
+ setValue(value);
48
+ break;
49
+ case 'ArrowLeft':
50
+ case 'ArrowRight':
51
+ case 'Tab':
52
+ case 'Backspace':
53
+ return;
54
+ default:
55
+ }
56
+ if (!key.match(/^[\-\d\.]*$/)) {
57
+ e.preventDefault(); // kill anything that's not a number
58
+ }
59
+ },
60
+ onChangeText = (value) => {
61
+ if (value === '') {
62
+ value = 0; // empty string makes value null
63
+ } else if (value.match(/\.$/)) { // value ends with a decimal point
64
+ // don't parseFloat, otherwise we'll lose the decimal point
65
+ } else if (value.match(/0$/)) { // value ends with a zero
66
+ // don't parseFloat, otherwise we'll lose the ability to do things like 1.03
67
+ } else {
68
+ value = parseFloat(value, 10);
69
+ }
70
+ if (value < minValue) {
71
+ value = minValue;
72
+ } else if (value > maxValue) {
73
+ value = maxValue;
74
+ }
75
+ setLocalValue(value);
76
+ debouncedSetValueRef.current(value);
77
+ },
78
+ onDecrement = () => {
79
+ let localValue = value;
80
+ if (minValue && localValue === minValue) {
81
+ return;
82
+ }
83
+ if (!localValue) {
84
+ localValue = 0;
85
+ }
86
+ localValue = parseFloat(localValue, 10) - step;
87
+ if (minValue > localValue) {
88
+ localValue = minValue;
89
+ }
90
+ setValue(localValue);
91
+ },
92
+ onIncrement = () => {
93
+ let localValue = value;
94
+ if (maxValue && localValue === maxValue) {
95
+ return;
96
+ }
97
+ if (!localValue) {
98
+ localValue = 0;
99
+ }
100
+ localValue = parseFloat(localValue, 10) + step;
101
+ if (maxValue < localValue) {
102
+ localValue = maxValue;
103
+ }
104
+ setValue(localValue);
105
+ };
106
+
107
+ useEffect(() => {
108
+ // Set up debounce fn
109
+ // Have to do this because otherwise, lodash tries to create a debounced version of the fn from only this render
110
+ debouncedSetValueRef.current?.cancel(); // Cancel any previous debounced fn
111
+ debouncedSetValueRef.current = _.debounce(setValue, autoSubmitDelay);
112
+ }, [setValue]);
113
+
114
+ useEffect(() => {
115
+
116
+ // Make local value conform to externally changed value
117
+ if (value !== localValue) {
118
+ setLocalValue(value);
119
+ }
120
+
121
+ }, [value]);
122
+
123
+ let sliderValue = value;
124
+
125
+ if (localValue === null || typeof localValue === 'undefined') {
126
+ localValue = ''; // If the value is null or undefined, don't let this be an uncontrolled input
127
+ }
128
+
129
+ if (sliderValue === null || typeof sliderValue === 'undefined') {
130
+ sliderValue = 0; // If the value is null or undefined, force slider to use zero
131
+ }
132
+
133
+ // convert localValue to string if necessary, because numbers work on web but not mobile; while strings work in both places
134
+ let inputValue = localValue;
135
+ if (_.isNumber(inputValue)) {
136
+ inputValue = '' + inputValue;
137
+ }
138
+
139
+ const sizeProps = {};
140
+ if (!props.flex && !props.w) {
141
+ sizeProps.flex = 1;
142
+ }
143
+
144
+ if (sliderValue === 0) {
145
+ sliderValue = FAKE_ZERO; // Slider doesn't like zero
146
+ }
147
+
148
+ return <Row
149
+ w="100%"
150
+ alignItems="center"
151
+ {...propsToPass}
152
+ >
153
+ <InputWithTooltip
154
+ {...testProps('readout')}
155
+ value={inputValue}
156
+ onChangeText={onChangeText}
157
+ onKeyPress={onInputKeyPress}
158
+ h="100%"
159
+ w="50px"
160
+ p={2}
161
+ mr={4}
162
+ bg={styles.FORM_INPUT_BG}
163
+ _focus={{
164
+ bg: styles.FORM_INPUT_FOCUS_BG,
165
+ }}
166
+ fontSize={styles.SLIDER_READOUT_FONTSIZE}
167
+ textAlign="center"
168
+ borderRadius="md"
169
+ borderWidth={1}
170
+ borderColor="#bbb"
171
+ isDisabled={isDisabled}
172
+ {...props._input}
173
+ />
174
+ <Row flex={1}>
175
+ <Slider
176
+ {...testProps('slider')}
177
+ ref={props.outerRef}
178
+
179
+ style={{
180
+ width: '100%',
181
+ height: 40,
182
+ }}
183
+ minimumTrackTintColor={styles.SLIDER_MIN_TRACK_COLOR}
184
+ maximumTrackTintColor={styles.SLIDER_MAX_TRACK_COLOR}
185
+ thumbTintColor={styles.SLIDER_THUMB_COLOR}
186
+ minimumValue={minValue}
187
+ maximumValue={maxValue}
188
+ step={step}
189
+ value={sliderValue}
190
+ onValueChange={(value) => {
191
+ // This sets the localValue, only for display purposes
192
+ setLocalValue(value);
193
+ }}
194
+ onSlidingComplete={(value) => {
195
+ // This sets the actual value
196
+ if (value === FAKE_ZERO) {
197
+ value = 0;
198
+ }
199
+ setValue(value);
200
+ }}
201
+ />
202
+ </Row>
203
+ </Row>;
204
+ },
205
+ SliderField = withComponent(withValue(SliderElement));
206
+
207
+ // Tooltip needs us to forwardRef
208
+ export default withTooltip(React.forwardRef((props, ref) => {
209
+ return <SliderField {...props} outerRef={ref} />;
210
+ }));
@@ -0,0 +1,177 @@
1
+ import React, { useState, useRef, useEffect, } from 'react';
2
+ import {
3
+ Box,
4
+ Button,
5
+ Column,
6
+ Icon,
7
+ Modal,
8
+ Row,
9
+ Text,
10
+ } from 'native-base';
11
+ import Panel from '../Panel/Panel.js';
12
+ import TriangleExclamation from '../Icons/TriangleExclamation.js';
13
+ import useAdjustedWindowSize from '../../Hooks/useAdjustedWindowSize.js';
14
+ import testProps from '../../Functions/testProps.js';
15
+ import _ from 'lodash';
16
+
17
+ export default function withModal(WrappedComponent) {
18
+ return (props) => {
19
+
20
+ if (props.disableWithModal || props.showModal) {
21
+ return <WrappedComponent {...props} />;
22
+ }
23
+
24
+ const
25
+ [title, setTitle] = useState(''),
26
+ [message, setMessage] = useState(''),
27
+ [canClose, setCanClose] = useState(true),
28
+ [includeCancel, setIncludeCancel] = useState(false),
29
+ [isModalShown, setIsModalShown] = useState(false),
30
+ [onOk, setOnOk] = useState(),
31
+ [onYes, setOnYes] = useState(),
32
+ [onNo, setOnNo] = useState(),
33
+ [customButtons, setCustomButtons] = useState(),
34
+ [color, setColor] = useState('#000'),
35
+ autoFocusRef = useRef(null),
36
+ cancelRef = useRef(null),
37
+ [width, height] = useAdjustedWindowSize(400, 250),
38
+ onCancel = () => {
39
+ setIsModalShown(false);
40
+ },
41
+ showModal = (args) => {
42
+ const {
43
+ title = '',
44
+ message = '',
45
+ canClose = true,
46
+ includeCancel = false,
47
+ onOk,
48
+ onYes,
49
+ onNo,
50
+ customButtons,
51
+ color,
52
+ } = args;
53
+
54
+ if (title) {
55
+ setTitle(title);
56
+ }
57
+ if (!message) {
58
+ throw new Error('Message is required for showModal');
59
+ }
60
+ setMessage(message);
61
+ setCanClose(canClose);
62
+ setIncludeCancel(includeCancel);
63
+ if (onOk) {
64
+ setOnOk(() => onOk);
65
+ }
66
+ if (onYes) {
67
+ setOnYes(() => onYes);
68
+ }
69
+ if (onNo) {
70
+ setOnNo(() => onNo);
71
+ }
72
+ if (customButtons) {
73
+ setCustomButtons(customButtons);
74
+ }
75
+ if (color) {
76
+ setColor(color);
77
+ }
78
+
79
+ setIsModalShown(true);
80
+ };
81
+
82
+ let buttons = [];
83
+ if (isModalShown) {
84
+ if (includeCancel) {
85
+ buttons.push(<Button
86
+ {...testProps('cancelBtn')}
87
+ key="cancelBtn"
88
+ onPress={onCancel}
89
+ color="#fff"
90
+ colorScheme="coolGray"
91
+ variant="ghost" // or unstyled
92
+ ref={cancelRef}
93
+ >Cancel</Button>);
94
+ }
95
+ if (onOk) {
96
+ buttons.push(<Button
97
+ {...testProps('okBtn')}
98
+ key="okBtn"
99
+ ref={autoFocusRef}
100
+ onPress={onOk}
101
+ color="#fff"
102
+ >OK</Button>);
103
+ }
104
+ if (onNo) {
105
+ buttons.push(<Button
106
+ {...testProps('noBtn')}
107
+ key="noBtn"
108
+ onPress={onNo}
109
+ color="trueGray.800"
110
+ variant="ghost"
111
+ // colorScheme="neutral"
112
+ mr={2}
113
+ >No</Button>);
114
+ }
115
+ if (onYes) {
116
+ buttons.push(<Button
117
+ {...testProps('yesBtn')}
118
+ key="yesBtn"
119
+ ref={autoFocusRef}
120
+ onPress={onYes}
121
+ color="#fff"
122
+ // colorScheme="danger"
123
+ >Yes</Button>);
124
+ }
125
+ if (customButtons) {
126
+ _.each(customButtons, (button) => {
127
+ buttons.push(button);
128
+ });
129
+ }
130
+ }
131
+
132
+ return <>
133
+ <WrappedComponent
134
+ {...props}
135
+ disableWithModal={false}
136
+ showModal={showModal}
137
+ hideModal={onCancel}
138
+ />
139
+ {isModalShown &&
140
+ <Modal
141
+ isOpen={true}
142
+ onClose={onCancel}
143
+ >
144
+ <Panel
145
+ {...props}
146
+ reference="modal"
147
+ isCollapsible={false}
148
+ bg="#fff"
149
+ w={width}
150
+ h={height}
151
+ flex={null}
152
+ >
153
+
154
+ {title && <Modal.Header>{title}</Modal.Header>}
155
+ <Modal.Body
156
+ borderTopWidth={0}
157
+ bg="#fff"
158
+ p={3}
159
+ justifyContent="center"
160
+ alignItems="center"
161
+ borderRadius={5}
162
+ flexDirection="row"
163
+ >
164
+ <Box w="50px" mx={2}>
165
+ <Icon as={TriangleExclamation} color={color} size="10" />
166
+ </Box>
167
+ <Text flex={1} color={color} fontSize="18px">{message}</Text>
168
+ </Modal.Body>
169
+ <Modal.Footer py={2} pr={4} justifyContent="flex-end">
170
+ {buttons}
171
+ </Modal.Footer>
172
+ </Panel>
173
+ </Modal>}
174
+
175
+ </>;
176
+ };
177
+ }
@@ -222,6 +222,7 @@ import Panel from './Panel/Panel.js';
222
222
  // import Picker from '../Components/Panel/Picker.js';
223
223
  import PlusMinusButton from './Buttons/PlusMinusButton.js';
224
224
  import RadioGroup from './Form/Field/RadioGroup/RadioGroup.js';
225
+ import Slider from './Form/Field/Slider.js';
225
226
  import SquareButton from './Buttons/SquareButton.js';
226
227
  import TabPanel from './Panel/TabPanel.js';
227
228
  import Tag from './Form/Field/Tag/Tag.js';
@@ -457,6 +458,7 @@ const components = {
457
458
  // Picker,
458
459
  PlusMinusButton,
459
460
  RadioGroup,
461
+ Slider,
460
462
  Row,
461
463
  SquareButton,
462
464
  TabPanel,
@@ -98,6 +98,10 @@ const defaults = {
98
98
  REORDER_BORDER_COLOR: '#23d9ea',
99
99
  REORDER_BORDER_WIDTH: 4,
100
100
  REORDER_BORDER_STYLE: 'dashed',
101
+ SLIDER_MIN_TRACK_COLOR: '#000',
102
+ SLIDER_MAX_TRACK_COLOR: '#ccc',
103
+ SLIDER_THUMB_COLOR: '#000',
104
+ SLIDER_READOUT_FONTSIZE: DEFAULT_FONTSIZE,
101
105
  TAB_ACTIVE_BG: 'trueGray.200',
102
106
  TAB_ACTIVE_HOVER_BG: 'trueGray.200',
103
107
  TAB_ACTIVE_COLOR: 'primary.800',