@hero-design/rn 7.10.2 → 7.12.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 (103) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/assets/fonts/hero-icons.ttf +0 -0
  3. package/es/index.js +3778 -728
  4. package/global-setup.js +3 -0
  5. package/jest.config.js +1 -0
  6. package/lib/assets/fonts/hero-icons.ttf +0 -0
  7. package/lib/index.js +3779 -726
  8. package/package.json +7 -3
  9. package/rollup.config.js +8 -1
  10. package/src/components/ContentNavigator/__tests__/__snapshots__/index.spec.tsx.snap +2 -0
  11. package/src/components/ContentNavigator/__tests__/index.spec.tsx +19 -2
  12. package/src/components/ContentNavigator/index.tsx +12 -1
  13. package/src/components/FAB/ActionGroup/__tests__/__snapshots__/index.spec.tsx.snap +4 -0
  14. package/src/components/FAB/ActionGroup/index.tsx +16 -5
  15. package/src/components/Icon/HeroIcon/selection.json +1 -1
  16. package/src/components/Icon/IconList.ts +1 -0
  17. package/src/components/PinInput/PinCell.tsx +34 -0
  18. package/src/components/PinInput/StyledPinInput.tsx +88 -0
  19. package/src/components/PinInput/__tests__/PinCell.spec.tsx +48 -0
  20. package/src/components/PinInput/__tests__/StyledPinInput.spec.tsx +22 -0
  21. package/src/components/PinInput/__tests__/__snapshots__/PinCell.spec.tsx.snap +186 -0
  22. package/src/components/PinInput/__tests__/__snapshots__/StyledPinInput.spec.tsx.snap +58 -0
  23. package/src/components/PinInput/__tests__/__snapshots__/index.spec.tsx.snap +1028 -0
  24. package/src/components/PinInput/__tests__/index.spec.tsx +91 -0
  25. package/src/components/PinInput/index.tsx +173 -0
  26. package/src/components/Select/MultiSelect/Option.tsx +1 -1
  27. package/src/components/Select/MultiSelect/OptionList.tsx +48 -26
  28. package/src/components/Select/MultiSelect/__tests__/OptionList.spec.tsx +13 -0
  29. package/src/components/Select/MultiSelect/__tests__/__snapshots__/OptionList.spec.tsx.snap +1062 -556
  30. package/src/components/Select/MultiSelect/__tests__/__snapshots__/index.spec.tsx.snap +983 -889
  31. package/src/components/Select/MultiSelect/index.tsx +59 -31
  32. package/src/components/Select/SingleSelect/OptionList.tsx +45 -26
  33. package/src/components/Select/SingleSelect/__tests__/OptionList.spec.tsx +8 -0
  34. package/src/components/Select/SingleSelect/__tests__/__snapshots__/OptionList.spec.tsx.snap +992 -500
  35. package/src/components/Select/SingleSelect/__tests__/__snapshots__/index.spec.tsx.snap +880 -786
  36. package/src/components/Select/SingleSelect/index.tsx +60 -31
  37. package/src/components/Select/StyledOptionList.tsx +88 -0
  38. package/src/components/Select/StyledSelect.tsx +18 -16
  39. package/src/components/Select/__tests__/StyledSelect.spec.tsx +1 -14
  40. package/src/components/Select/__tests__/__snapshots__/StyledSelect.spec.tsx.snap +0 -13
  41. package/src/components/Select/types.tsx +47 -0
  42. package/src/components/TextInput/__tests__/index.spec.tsx +15 -0
  43. package/src/components/TextInput/index.tsx +20 -16
  44. package/src/components/TimePicker/StyledTimePicker.tsx +8 -0
  45. package/src/components/TimePicker/TimePickerAndroid.tsx +61 -0
  46. package/src/components/TimePicker/TimePickerIOS.tsx +91 -0
  47. package/src/components/TimePicker/__tests__/TimePicker.spec.tsx +34 -0
  48. package/src/components/TimePicker/__tests__/TimePickerAndroid.spec.tsx +39 -0
  49. package/src/components/TimePicker/__tests__/TimePickerIOS.spec.tsx +46 -0
  50. package/src/components/TimePicker/__tests__/__snapshots__/TimePickerAndroid.spec.tsx.snap +200 -0
  51. package/src/components/TimePicker/__tests__/__snapshots__/TimePickerIOS.spec.tsx.snap +513 -0
  52. package/src/components/TimePicker/index.tsx +15 -0
  53. package/src/components/TimePicker/types.ts +50 -0
  54. package/src/components/Typography/Text/StyledText.tsx +1 -1
  55. package/src/components/Typography/Text/__tests__/StyledText.spec.tsx +1 -0
  56. package/src/components/Typography/Text/__tests__/__snapshots__/StyledText.spec.tsx.snap +22 -0
  57. package/src/components/Typography/Text/index.tsx +1 -1
  58. package/src/index.ts +4 -0
  59. package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +44 -0
  60. package/src/theme/components/pinInput.ts +45 -0
  61. package/src/theme/components/select.ts +4 -0
  62. package/src/theme/components/timePicker.ts +11 -0
  63. package/src/theme/components/typography.ts +2 -0
  64. package/src/theme/global/colors.ts +1 -1
  65. package/src/theme/global/space.ts +10 -10
  66. package/src/theme/index.ts +9 -3
  67. package/testUtils/setup.tsx +10 -0
  68. package/types/components/ContentNavigator/index.d.ts +5 -1
  69. package/types/components/Icon/IconList.d.ts +1 -1
  70. package/types/components/Icon/utils.d.ts +1 -1
  71. package/types/components/PinInput/PinCell.d.ts +8 -0
  72. package/types/components/PinInput/StyledPinInput.d.ts +73 -0
  73. package/types/components/PinInput/__tests__/PinCell.spec.d.ts +1 -0
  74. package/types/components/PinInput/__tests__/StyledPinInput.spec.d.ts +1 -0
  75. package/types/components/PinInput/__tests__/index.spec.d.ts +1 -0
  76. package/types/components/PinInput/index.d.ts +48 -0
  77. package/types/components/Select/MultiSelect/OptionList.d.ts +1 -1
  78. package/types/components/Select/MultiSelect/index.d.ts +3 -25
  79. package/types/components/Select/SingleSelect/OptionList.d.ts +1 -1
  80. package/types/components/Select/SingleSelect/index.d.ts +4 -26
  81. package/types/components/Select/StyledOptionList.d.ts +17 -0
  82. package/types/components/Select/StyledSelect.d.ts +7 -7
  83. package/types/components/Select/index.d.ts +1 -1
  84. package/types/components/Select/types.d.ts +44 -0
  85. package/types/components/TimePicker/StyledTimePicker.d.ts +8 -0
  86. package/types/components/TimePicker/TimePickerAndroid.d.ts +3 -0
  87. package/types/components/TimePicker/TimePickerIOS.d.ts +3 -0
  88. package/types/components/TimePicker/__tests__/TimePicker.spec.d.ts +1 -0
  89. package/types/components/TimePicker/__tests__/TimePickerAndroid.spec.d.ts +1 -0
  90. package/types/components/TimePicker/__tests__/TimePickerIOS.spec.d.ts +1 -0
  91. package/types/components/TimePicker/index.d.ts +3 -0
  92. package/types/components/TimePicker/types.d.ts +49 -0
  93. package/types/components/Typography/Text/StyledText.d.ts +1 -1
  94. package/types/components/Typography/Text/index.d.ts +1 -1
  95. package/types/index.d.ts +3 -1
  96. package/types/theme/components/pinInput.d.ts +35 -0
  97. package/types/theme/components/select.d.ts +4 -0
  98. package/types/theme/components/timePicker.d.ts +6 -0
  99. package/types/theme/components/typography.d.ts +2 -0
  100. package/types/theme/index.d.ts +6 -2
  101. package/src/components/Select/types.ts +0 -1
  102. package/src/components/TextInput/__tests__/.log/ti-10343.log +0 -62
  103. package/src/components/TextInput/__tests__/.log/tsserver.log +0 -15584
@@ -1,55 +1,64 @@
1
- import React, { useState } from 'react';
2
- import { StyleProp, ViewStyle, TouchableOpacity, View } from 'react-native';
1
+ import React, { useEffect, useState } from 'react';
2
+ import { TouchableOpacity, Keyboard, KeyboardEvent, View } from 'react-native';
3
3
 
4
- import { OptionType } from '../types';
4
+ import { SelectProps } from '../types';
5
5
  import BottomSheet from '../../BottomSheet';
6
6
  import OptionList from './OptionList';
7
7
  import TextInput from '../../TextInput';
8
+ import { StyledSearchBar } from '../StyledSelect';
8
9
 
9
- export interface SingleSelectProps<T> {
10
- /**
11
- * An array of options to be selected.
12
- */
13
- options: OptionType<T>[];
10
+ export interface SingleSelectProps<T> extends SelectProps<T> {
14
11
  /**
15
12
  * Current selected value.
16
13
  */
17
14
  value: T | null;
18
15
  /**
19
- * event handler for confirm button
16
+ * on select event handler
20
17
  */
21
18
  onConfirm: (value: T | null) => void;
22
- /**
23
- * Field label.
24
- */
25
- label: string;
26
- /**
27
- * Used to extract a unique key for a given option at the specified index. Key is used for caching and as the react key to track item re-ordering.
28
- * The default extractor checks option.key, and then falls back to using the index, like React does.
29
- */
30
- keyExtractor?: (option: OptionType<T>, index?: number) => string;
31
- /**
32
- * Additional style.
33
- */
34
- style?: StyleProp<ViewStyle>;
35
- /**
36
- * Testing id of the component.
37
- */
38
- testID?: string;
39
19
  }
40
20
 
41
21
  const SingleSelect = <T,>({
42
- options,
43
- value,
44
- testID,
45
- style,
46
22
  label,
23
+ loading,
47
24
  onConfirm,
25
+ onDimiss,
26
+ onEndReached,
27
+ onQueryChange,
28
+ options,
29
+ query,
30
+ style,
31
+ testID,
32
+ value,
48
33
  }: SingleSelectProps<T>) => {
49
34
  const [open, setOpen] = useState(false);
50
35
  const [selectingValue, setSelectingValue] = useState<T | null>(value);
51
36
  const displayedValue = options.find(opt => value === opt.value)?.text;
52
37
 
38
+ const [isKeyboardVisible, setKeyboardVisible] = useState(false);
39
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
40
+
41
+ useEffect(() => {
42
+ const keyboardDidShowListener = Keyboard.addListener(
43
+ 'keyboardWillShow',
44
+ (e: KeyboardEvent) => {
45
+ setKeyboardVisible(true);
46
+ setKeyboardHeight(e.endCoordinates.height);
47
+ }
48
+ );
49
+ const keyboardDidHideListener = Keyboard.addListener(
50
+ 'keyboardWillHide',
51
+ () => {
52
+ setKeyboardVisible(false);
53
+ }
54
+ );
55
+
56
+ return () => {
57
+ keyboardDidHideListener.remove();
58
+ keyboardDidShowListener.remove();
59
+ };
60
+ }, []);
61
+
53
62
  return (
54
63
  <TouchableOpacity onPress={() => setOpen(true)}>
55
64
  <View pointerEvents="none">
@@ -65,10 +74,30 @@ const SingleSelect = <T,>({
65
74
  <BottomSheet
66
75
  open={open}
67
76
  onRequestClose={() => setOpen(false)}
68
- onDismiss={() => setSelectingValue(value)}
77
+ onDismiss={() => {
78
+ setSelectingValue(value);
79
+ if (onDimiss) onDimiss();
80
+ }}
69
81
  header={label}
82
+ style={{
83
+ paddingBottom: isKeyboardVisible ? keyboardHeight : 0,
84
+ }}
70
85
  >
86
+ {onQueryChange && (
87
+ <StyledSearchBar>
88
+ <TextInput
89
+ editable
90
+ placeholder="Search"
91
+ suffix="search-outlined"
92
+ onChangeText={onQueryChange}
93
+ value={query}
94
+ />
95
+ </StyledSearchBar>
96
+ )}
71
97
  <OptionList
98
+ onQueryChange={onQueryChange}
99
+ onEndReached={onEndReached}
100
+ loading={loading}
72
101
  options={options}
73
102
  value={selectingValue}
74
103
  onPress={selectedValue => {
@@ -0,0 +1,88 @@
1
+ import { useTheme } from '@emotion/react';
2
+ import React, { useRef, useState } from 'react';
3
+ import { Dimensions, FlatList, ListRenderItemInfo, View } from 'react-native';
4
+ import Spinner from '../Spinner';
5
+ import { getKey } from './helpers';
6
+ import { OptionType, SelectProps } from './types';
7
+
8
+ export interface RenderItemProps<T> {
9
+ item: OptionType<T>;
10
+ }
11
+
12
+ export interface OptionListProps<T> extends SelectProps<T> {
13
+ /**
14
+ * FlatList renderItem Element
15
+ */
16
+ RenderItem: React.FC<RenderItemProps<T>>;
17
+ /**
18
+ * Selected scroll index
19
+ */
20
+ scrollIndex?: number;
21
+ }
22
+
23
+ const StyledOptionList = <T,>({
24
+ keyExtractor,
25
+ loading,
26
+ onEndReached,
27
+ onQueryChange,
28
+ options,
29
+ RenderItem,
30
+ scrollIndex = 0,
31
+ }: Pick<
32
+ OptionListProps<T>,
33
+ | 'scrollIndex'
34
+ | 'keyExtractor'
35
+ | 'loading'
36
+ | 'onEndReached'
37
+ | 'options'
38
+ | 'RenderItem'
39
+ | 'onQueryChange'
40
+ >) => {
41
+ const theme = useTheme();
42
+ const flatListRef = useRef<FlatList>(null);
43
+
44
+ const [onEndReachedCalled, setOnEndReachedCalled] = useState(false);
45
+
46
+ return (
47
+ <FlatList
48
+ ref={flatListRef}
49
+ style={{
50
+ paddingHorizontal: theme.__hd__.select.space.optionListPadding,
51
+ ...(onQueryChange ? { height: Dimensions.get('screen').height } : {}),
52
+ }}
53
+ data={options}
54
+ keyExtractor={keyExtractor}
55
+ onEndReachedThreshold={0.1}
56
+ onEndReached={() => setOnEndReachedCalled(true)}
57
+ onScrollToIndexFailed={() => {}}
58
+ onContentSizeChange={() =>
59
+ options.length &&
60
+ flatListRef.current?.scrollToIndex({ index: scrollIndex })
61
+ }
62
+ onMomentumScrollBegin={() => {
63
+ if (onEndReached && onEndReachedCalled && !loading) onEndReached();
64
+ setOnEndReachedCalled(false);
65
+ }}
66
+ ListFooterComponent={
67
+ loading ? (
68
+ <View
69
+ style={{
70
+ display: 'flex',
71
+ alignItems: 'center',
72
+ height: theme.space.xxxxlarge,
73
+ }}
74
+ >
75
+ <Spinner />
76
+ </View>
77
+ ) : null
78
+ }
79
+ renderItem={({ item, index }: ListRenderItemInfo<OptionType<T>>) => (
80
+ <View key={getKey(item, index, keyExtractor)}>
81
+ <RenderItem item={item} />
82
+ </View>
83
+ )}
84
+ />
85
+ );
86
+ };
87
+
88
+ export default StyledOptionList;
@@ -2,21 +2,17 @@ import { View, TouchableOpacity } from 'react-native';
2
2
  import styled from '@emotion/native';
3
3
  import Typography from '../Typography';
4
4
 
5
- const OptionWrapper = styled(TouchableOpacity)<{ themeSelected: boolean }>(
6
- ({ theme, themeSelected }) => ({
7
- flexDirection: 'row',
8
- justifyContent: 'space-between',
9
- alignItems: 'center',
10
- borderRadius: theme.__hd__.select.radii.option,
11
- padding: theme.__hd__.select.space.optionPadding,
12
- backgroundColor: themeSelected
13
- ? theme.__hd__.select.colors.checkedOption
14
- : theme.__hd__.select.colors.option,
15
- })
16
- );
17
-
18
- const OptionListWrapper = styled(View)(({ theme }) => ({
19
- padding: theme.__hd__.select.space.optionListPadding,
5
+ const OptionWrapper = styled(TouchableOpacity)<{
6
+ themeSelected: boolean;
7
+ }>(({ theme, themeSelected }) => ({
8
+ flexDirection: 'row',
9
+ justifyContent: 'space-between',
10
+ alignItems: 'center',
11
+ borderRadius: theme.__hd__.select.radii.option,
12
+ padding: theme.__hd__.select.space.optionPadding,
13
+ backgroundColor: themeSelected
14
+ ? theme.__hd__.select.colors.checkedOption
15
+ : theme.__hd__.select.colors.option,
20
16
  }));
21
17
 
22
18
  const Spacer = styled(View)(({ theme }) => ({
@@ -27,4 +23,10 @@ const FooterText = styled(Typography.Text)(({ theme }) => ({
27
23
  color: theme.__hd__.select.colors.footerText,
28
24
  }));
29
25
 
30
- export { OptionWrapper, OptionListWrapper, Spacer, FooterText };
26
+ const StyledSearchBar = styled(View)(({ theme }) => ({
27
+ marginTop: theme.__hd__.select.space.searchBarMarginTopSpacing,
28
+ paddingHorizontal: theme.__hd__.select.space.searchBarHorizontalSpacing,
29
+ paddingBottom: theme.__hd__.select.space.searchBarBottomSpacing,
30
+ }));
31
+
32
+ export { OptionWrapper, Spacer, FooterText, StyledSearchBar };
@@ -1,11 +1,6 @@
1
1
  import React from 'react';
2
2
  import renderWithTheme from '../../../testHelpers/renderWithTheme';
3
- import {
4
- OptionWrapper,
5
- OptionListWrapper,
6
- Spacer,
7
- FooterText,
8
- } from '../StyledSelect';
3
+ import { OptionWrapper, Spacer, FooterText } from '../StyledSelect';
9
4
 
10
5
  describe('OptionWrapper', () => {
11
6
  it.each`
@@ -21,14 +16,6 @@ describe('OptionWrapper', () => {
21
16
  });
22
17
  });
23
18
 
24
- describe('OptionListWrapper', () => {
25
- it('has correct style', () => {
26
- const { toJSON } = renderWithTheme(<OptionListWrapper />);
27
-
28
- expect(toJSON()).toMatchSnapshot();
29
- });
30
- });
31
-
32
19
  describe('Spacer', () => {
33
20
  it('has correct style', () => {
34
21
  const { toJSON } = renderWithTheme(<Spacer />);
@@ -27,19 +27,6 @@ exports[`FooterText has correct style 1`] = `
27
27
  </Text>
28
28
  `;
29
29
 
30
- exports[`OptionListWrapper has correct style 1`] = `
31
- <View
32
- style={
33
- Array [
34
- Object {
35
- "padding": 16,
36
- },
37
- undefined,
38
- ]
39
- }
40
- />
41
- `;
42
-
43
30
  exports[`OptionWrapper has selected style: false 1`] = `
44
31
  <View
45
32
  accessible={true}
@@ -0,0 +1,47 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export type OptionType<T> = { value: T; text: string; key?: string };
4
+
5
+ export interface SelectProps<T> {
6
+ /**
7
+ * An array of options to be selected.
8
+ */
9
+ options: OptionType<T>[];
10
+ /**
11
+ * Used to extract a unique key for a given option at the specified index. Key is used for caching and as the react key to track item re-ordering.
12
+ * The default extractor checks option.key, and then falls back to using the index, like React does.
13
+ */
14
+ keyExtractor?: (option: OptionType<T>, index?: number) => string;
15
+ /**
16
+ * Current search value.
17
+ */
18
+ query?: string;
19
+ /**
20
+ * Search bar onChangeText event handler
21
+ */
22
+ onQueryChange?: (value: string) => void;
23
+ /**
24
+ * Event handler when selection dimiss
25
+ */
26
+ onDimiss?: () => void;
27
+ /**
28
+ * Event handler when end of the list reached
29
+ */
30
+ onEndReached?: () => void;
31
+ /**
32
+ * Show indicator at bottom of option list
33
+ */
34
+ loading?: boolean;
35
+ /**
36
+ * Field label.
37
+ */
38
+ label: string;
39
+ /**
40
+ * Additional style.
41
+ */
42
+ style?: StyleProp<ViewStyle>;
43
+ /**
44
+ * Testing id of the component.
45
+ */
46
+ testID?: string;
47
+ }
@@ -60,6 +60,21 @@ describe('TextInput', () => {
60
60
  ).toHaveLength(1);
61
61
  });
62
62
 
63
+ it('should not render input-label if label is empty', () => {
64
+ const { getByTestId } = renderWithTheme(
65
+ <TextInput
66
+ prefix="dollar-sign"
67
+ suffix="arrow-down"
68
+ testID="idle-text-input"
69
+ />
70
+ );
71
+
72
+ expect(getByTestId('idle-text-input')).toBeTruthy();
73
+ expect(
74
+ within(getByTestId('idle-text-input')).queryAllByTestId('input-label')
75
+ ).toHaveLength(0);
76
+ });
77
+
63
78
  it('onChangeText, onBlur, onFocus', () => {
64
79
  const onChangeText = jest.fn();
65
80
  const onBlur = jest.fn();
@@ -171,14 +171,16 @@ const TextInput = ({
171
171
  *
172
172
  </StyledAsteriskLabel>
173
173
  )}
174
- <StyledLabel
175
- nativeID={accessibilityLabelledBy}
176
- testID="input-label"
177
- fontSize="small"
178
- themeVariant={variant}
179
- >
180
- {label}
181
- </StyledLabel>
174
+ {!!label && (
175
+ <StyledLabel
176
+ nativeID={accessibilityLabelledBy}
177
+ testID="input-label"
178
+ fontSize="small"
179
+ themeVariant={variant}
180
+ >
181
+ {label}
182
+ </StyledLabel>
183
+ )}
182
184
  </StyledLabelContainer>
183
185
  )}
184
186
  {typeof prefix === 'string' ? (
@@ -200,14 +202,16 @@ const TextInput = ({
200
202
  *
201
203
  </StyledAsteriskLabelInsideTextInput>
202
204
  )}
203
- <StyledLabelInsideTextInput
204
- nativeID={accessibilityLabelledBy}
205
- testID="input-label"
206
- fontSize="medium"
207
- themeVariant={variant}
208
- >
209
- {label}
210
- </StyledLabelInsideTextInput>
205
+ {!!label && (
206
+ <StyledLabelInsideTextInput
207
+ nativeID={accessibilityLabelledBy}
208
+ testID="input-label"
209
+ fontSize="medium"
210
+ themeVariant={variant}
211
+ >
212
+ {label}
213
+ </StyledLabelInsideTextInput>
214
+ )}
211
215
  </StyledLabelContainerInsideTextInput>
212
216
  )}
213
217
  <StyledTextInput
@@ -0,0 +1,8 @@
1
+ import styled from '@emotion/native';
2
+ import { View, ViewProps } from 'react-native';
3
+
4
+ const StyledPickerWrapper = styled(View)<ViewProps>(({ theme }) => ({
5
+ height: theme.__hd__.timePicker.sizes.height,
6
+ }));
7
+
8
+ export { StyledPickerWrapper };
@@ -0,0 +1,61 @@
1
+ import DateTimePicker from '@react-native-community/datetimepicker';
2
+ import React, { useState } from 'react';
3
+ import { TouchableOpacity, View } from 'react-native';
4
+ import formatTime from 'date-fns/fp/format';
5
+
6
+ import TextInput from '../TextInput';
7
+ import { TimePickerProps } from './types';
8
+
9
+ const TimePickerAndroid = ({
10
+ value,
11
+ label,
12
+ placeholder,
13
+ onChange,
14
+ displayFormat = 'hh:mm aa',
15
+ disabled = false,
16
+ required,
17
+ error,
18
+ style,
19
+ testID,
20
+ }: TimePickerProps) => {
21
+ const [open, setOpen] = useState(false);
22
+
23
+ const is12Hour = displayFormat.includes('hh');
24
+ const displayValue = value ? formatTime(displayFormat, value) : '';
25
+ const pickerInitValue = value || new Date();
26
+
27
+ return (
28
+ <TouchableOpacity onPress={() => setOpen(true)} disabled={disabled}>
29
+ <View pointerEvents="none" testID="timePickerInputAndroid">
30
+ <TextInput
31
+ label={label}
32
+ value={displayValue}
33
+ suffix="clock-3"
34
+ placeholder={placeholder || displayFormat}
35
+ disabled={disabled}
36
+ error={error}
37
+ required={required}
38
+ style={style}
39
+ testID={testID}
40
+ />
41
+ </View>
42
+ {open ? (
43
+ <DateTimePicker
44
+ testID="timePickerAndroid"
45
+ mode="time"
46
+ value={pickerInitValue}
47
+ display="default"
48
+ onChange={(_: any, date: Date | undefined) => {
49
+ setOpen(false);
50
+ if (date) {
51
+ onChange(date);
52
+ }
53
+ }}
54
+ is24Hour={!is12Hour}
55
+ />
56
+ ) : null}
57
+ </TouchableOpacity>
58
+ );
59
+ };
60
+
61
+ export default TimePickerAndroid;
@@ -0,0 +1,91 @@
1
+ import DateTimePicker from '@react-native-community/datetimepicker';
2
+ import React, { useState } from 'react';
3
+ import { TouchableOpacity, View } from 'react-native';
4
+ import formatTime from 'date-fns/fp/format';
5
+
6
+ import BottomSheet from '../BottomSheet';
7
+ import TextInput from '../TextInput';
8
+ import Typography from '../Typography';
9
+ import { StyledPickerWrapper } from './StyledTimePicker';
10
+ import { TimePickerProps } from './types';
11
+
12
+ const TimePickerIOS = ({
13
+ value,
14
+ label,
15
+ placeholder,
16
+ onChange,
17
+ confirmLabel,
18
+ displayFormat = 'hh:mm aa',
19
+ disabled = false,
20
+ required,
21
+ error,
22
+ style,
23
+ testID,
24
+ }: TimePickerProps) => {
25
+ const [selectingDate, setSelectingDate] = useState<Date | null>(value);
26
+ const [open, setOpen] = useState(false);
27
+
28
+ const is12Hour = displayFormat.includes('hh');
29
+ const displayValue = value ? formatTime(displayFormat, value) : '';
30
+
31
+ return (
32
+ <TouchableOpacity onPress={() => setOpen(true)} disabled={disabled}>
33
+ <View pointerEvents="none" testID="timePickerInputIOS">
34
+ <TextInput
35
+ label={label}
36
+ value={displayValue}
37
+ suffix="clock-3"
38
+ placeholder={placeholder || displayFormat}
39
+ disabled={disabled}
40
+ error={error}
41
+ required={required}
42
+ testID={testID}
43
+ style={style}
44
+ />
45
+ </View>
46
+ <BottomSheet
47
+ open={open}
48
+ onRequestClose={() => setOpen(false)}
49
+ header={label}
50
+ footer={
51
+ <TouchableOpacity
52
+ onPress={() => {
53
+ if (selectingDate) {
54
+ onChange(selectingDate);
55
+ }
56
+ setOpen(false);
57
+ }}
58
+ >
59
+ <Typography.Text
60
+ fontSize="large"
61
+ fontWeight="semi-bold"
62
+ intent="primary"
63
+ >
64
+ {confirmLabel}
65
+ </Typography.Text>
66
+ </TouchableOpacity>
67
+ }
68
+ >
69
+ <StyledPickerWrapper>
70
+ <DateTimePicker
71
+ testID="timePickerIOS"
72
+ value={selectingDate || new Date()}
73
+ mode="time"
74
+ // Current prop is24Hour config only available for Android.
75
+ // This is a work around to get the picker to display 24 hour format for iOS.
76
+ locale={is12Hour ? undefined : 'en-GB'}
77
+ onChange={(_: any, date: Date | undefined) => {
78
+ if (date) {
79
+ setSelectingDate(date);
80
+ }
81
+ }}
82
+ display="spinner"
83
+ style={{ flex: 1 }}
84
+ />
85
+ </StyledPickerWrapper>
86
+ </BottomSheet>
87
+ </TouchableOpacity>
88
+ );
89
+ };
90
+
91
+ export default TimePickerIOS;
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Platform } from 'react-native';
3
+ import TimePicker from '..';
4
+ import renderWithTheme from '../../../testHelpers/renderWithTheme';
5
+
6
+ describe('TimePicker', () => {
7
+ it('renders TimePickerIOS when OS is iOS', () => {
8
+ Platform.OS = 'ios';
9
+ const { getByTestId } = renderWithTheme(
10
+ <TimePicker
11
+ label="Start time"
12
+ value={new Date('December 17, 1995 03:24:00')}
13
+ confirmLabel="Confirm"
14
+ onChange={jest.fn()}
15
+ />
16
+ );
17
+
18
+ expect(getByTestId('timePickerInputIOS')).toBeDefined();
19
+ });
20
+
21
+ it('renders TimePickerAndroid when OS is android', () => {
22
+ Platform.OS = 'android';
23
+ const { getByTestId } = renderWithTheme(
24
+ <TimePicker
25
+ label="Start time"
26
+ value={new Date('December 17, 1995 03:24:00')}
27
+ confirmLabel="Confirm"
28
+ onChange={jest.fn()}
29
+ />
30
+ );
31
+
32
+ expect(getByTestId('timePickerInputAndroid')).toBeDefined();
33
+ });
34
+ });
@@ -0,0 +1,39 @@
1
+ import { fireEvent } from '@testing-library/react-native';
2
+ import React from 'react';
3
+ import renderWithTheme from '../../../testHelpers/renderWithTheme';
4
+ import TimePickerAndroid from '../TimePickerAndroid';
5
+
6
+ describe('TimePickerAndroid', () => {
7
+ it('renders correctly', () => {
8
+ const onChange = jest.fn();
9
+
10
+ const { getByText, queryByTestId, toJSON } = renderWithTheme(
11
+ <TimePickerAndroid
12
+ value={new Date('December 17, 1995 03:24:00')}
13
+ label="Break time"
14
+ confirmLabel="Confirm"
15
+ onChange={onChange}
16
+ />
17
+ );
18
+
19
+ expect(getByText('Break time')).toBeDefined();
20
+ expect(queryByTestId('text-input').props.value).toBe('03:24 AM');
21
+ expect(queryByTestId('timePickerAndroid')).toBeNull();
22
+
23
+ // Open time picker
24
+ fireEvent.press(getByText('Break time'));
25
+ expect(queryByTestId('timePickerAndroid')).toBeTruthy();
26
+
27
+ expect(toJSON()).toMatchSnapshot();
28
+
29
+ // Change time
30
+ fireEvent(
31
+ queryByTestId('timePickerAndroid'),
32
+ 'onChange',
33
+ null,
34
+ new Date('December 17, 1995 05:30:00')
35
+ );
36
+
37
+ expect(onChange).toBeCalledWith(new Date('December 17, 1995 05:30:00'));
38
+ });
39
+ });