@jobber/components-native 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/AtlantisContext/AtlantisContext.js +2 -0
- package/dist/src/Form/Form.js +187 -0
- package/dist/src/Form/Form.style.js +33 -0
- package/dist/src/Form/components/FormActionBar/FormActionBar.js +21 -0
- package/dist/src/Form/components/FormActionBar/FormActionBar.style.js +5 -0
- package/dist/src/Form/components/FormActionBar/index.js +1 -0
- package/dist/src/Form/components/FormBody/FormBody.js +20 -0
- package/dist/src/Form/components/FormBody/FormBody.style.js +26 -0
- package/dist/src/Form/components/FormBody/index.js +1 -0
- package/dist/src/Form/components/FormCache/FormCache.js +34 -0
- package/dist/src/Form/components/FormErrorBanner/FormErrorBanner.js +21 -0
- package/dist/src/Form/components/FormErrorBanner/index.js +1 -0
- package/dist/src/Form/components/FormErrorBanner/messages.js +13 -0
- package/dist/src/Form/components/FormMask/FormMask.js +11 -0
- package/dist/src/Form/components/FormMask/FormMask.style.js +15 -0
- package/dist/src/Form/components/FormMask/index.js +1 -0
- package/dist/src/Form/components/FormMessage/FormMessage.js +48 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.js +28 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.js +17 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/index.js +1 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/messages.js +8 -0
- package/dist/src/Form/components/FormMessage/index.js +1 -0
- package/dist/src/Form/components/FormMessageBanner/FormMessageBanner.js +15 -0
- package/dist/src/Form/components/FormMessageBanner/index.js +1 -0
- package/dist/src/Form/components/FormSaveButton/FormSaveButton.js +69 -0
- package/dist/src/Form/components/FormSaveButton/index.js +1 -0
- package/dist/src/Form/components/FormSaveButton/messages.js +8 -0
- package/dist/src/Form/constants.js +2 -0
- package/dist/src/Form/context/AtlantisFormContext.js +16 -0
- package/dist/src/Form/context/index.js +1 -0
- package/dist/src/Form/context/types.js +1 -0
- package/dist/src/Form/hooks/useFormViewRefs.js +14 -0
- package/dist/src/Form/hooks/useInternalForm.js +37 -0
- package/dist/src/Form/hooks/useOfflineHandler.js +24 -0
- package/dist/src/Form/hooks/useSaveButtonPosition.js +25 -0
- package/dist/src/Form/hooks/useScreenInformation.js +15 -0
- package/dist/src/Form/hooks/useScrollToError/index.js +1 -0
- package/dist/src/Form/hooks/useScrollToError/useScrollToError.js +63 -0
- package/dist/src/Form/index.js +4 -0
- package/dist/src/Form/messages.js +28 -0
- package/dist/src/Form/types.js +10 -0
- package/dist/src/InputDate/InputDate.js +76 -0
- package/dist/src/InputDate/index.js +1 -0
- package/dist/src/InputDate/messages.js +8 -0
- package/dist/src/Menu/Menu.js +67 -0
- package/dist/src/Menu/Menu.style.js +6 -0
- package/dist/src/Menu/components/MenuOption/MenuOption.js +25 -0
- package/dist/src/Menu/components/MenuOption/MenuOption.style.js +10 -0
- package/dist/src/Menu/components/MenuOption/index.js +1 -0
- package/dist/src/Menu/components/Overlay/Overlay.js +9 -0
- package/dist/src/Menu/components/Overlay/Overlay.style.js +6 -0
- package/dist/src/Menu/components/Overlay/index.js +1 -0
- package/dist/src/Menu/index.js +1 -0
- package/dist/src/Menu/messages.js +8 -0
- package/dist/src/Menu/types.js +1 -0
- package/dist/src/Menu/utils.js +84 -0
- package/dist/src/index.js +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/AtlantisContext/AtlantisContext.d.ts +7 -1
- package/dist/types/src/Form/Form.d.ts +4 -0
- package/dist/types/src/Form/Form.style.d.ts +31 -0
- package/dist/types/src/Form/components/FormActionBar/FormActionBar.d.ts +13 -0
- package/dist/types/src/Form/components/FormActionBar/FormActionBar.style.d.ts +15 -0
- package/dist/types/src/Form/components/FormActionBar/index.d.ts +2 -0
- package/dist/types/src/Form/components/FormBody/FormBody.d.ts +10 -0
- package/dist/types/src/Form/components/FormBody/FormBody.style.d.ts +24 -0
- package/dist/types/src/Form/components/FormBody/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormCache/FormCache.d.ts +10 -0
- package/dist/types/src/Form/components/FormErrorBanner/FormErrorBanner.d.ts +3 -0
- package/dist/types/src/Form/components/FormErrorBanner/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormErrorBanner/messages.d.ts +12 -0
- package/dist/types/src/Form/components/FormMask/FormMask.d.ts +2 -0
- package/dist/types/src/Form/components/FormMask/FormMask.style.d.ts +13 -0
- package/dist/types/src/Form/components/FormMask/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +19 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.d.ts +8 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.d.ts +20 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/messages.d.ts +7 -0
- package/dist/types/src/Form/components/FormMessage/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessageBanner/FormMessageBanner.d.ts +7 -0
- package/dist/types/src/Form/components/FormMessageBanner/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormSaveButton/FormSaveButton.d.ts +3 -0
- package/dist/types/src/Form/components/FormSaveButton/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormSaveButton/messages.d.ts +7 -0
- package/dist/types/src/Form/constants.d.ts +2 -0
- package/dist/types/src/Form/context/AtlantisFormContext.d.ts +12 -0
- package/dist/types/src/Form/context/index.d.ts +2 -0
- package/dist/types/src/Form/context/types.d.ts +26 -0
- package/dist/types/src/Form/hooks/useFormViewRefs.d.ts +10 -0
- package/dist/types/src/Form/hooks/useInternalForm.d.ts +19 -0
- package/dist/types/src/Form/hooks/useOfflineHandler.d.ts +1 -0
- package/dist/types/src/Form/hooks/useSaveButtonPosition.d.ts +12 -0
- package/dist/types/src/Form/hooks/useScreenInformation.d.ts +8 -0
- package/dist/types/src/Form/hooks/useScrollToError/index.d.ts +1 -0
- package/dist/types/src/Form/hooks/useScrollToError/useScrollToError.d.ts +10 -0
- package/dist/types/src/Form/index.d.ts +5 -0
- package/dist/types/src/Form/messages.d.ts +27 -0
- package/dist/types/src/Form/types.d.ts +199 -0
- package/dist/types/src/InputDate/InputDate.d.ts +74 -0
- package/dist/types/src/InputDate/index.d.ts +1 -0
- package/dist/types/src/InputDate/messages.d.ts +7 -0
- package/dist/types/src/InputNumber/InputNumber.d.ts +1 -1
- package/dist/types/src/Menu/Menu.d.ts +3 -0
- package/dist/types/src/Menu/Menu.style.d.ts +18 -0
- package/dist/types/src/Menu/components/MenuOption/MenuOption.d.ts +3 -0
- package/dist/types/src/Menu/components/MenuOption/MenuOption.style.d.ts +8 -0
- package/dist/types/src/Menu/components/MenuOption/index.d.ts +1 -0
- package/dist/types/src/Menu/components/Overlay/Overlay.d.ts +3 -0
- package/dist/types/src/Menu/components/Overlay/Overlay.style.d.ts +12 -0
- package/dist/types/src/Menu/components/Overlay/index.d.ts +1 -0
- package/dist/types/src/Menu/index.d.ts +2 -0
- package/dist/types/src/Menu/messages.d.ts +7 -0
- package/dist/types/src/Menu/types.d.ts +22 -0
- package/dist/types/src/Menu/utils.d.ts +10 -0
- package/dist/types/src/index.d.ts +3 -0
- package/package.json +3 -2
- package/src/AtlantisContext/AtlantisContext.tsx +10 -1
- package/src/Form/Form.style.ts +34 -0
- package/src/Form/Form.test.tsx +588 -0
- package/src/Form/Form.tsx +296 -0
- package/src/Form/components/FormActionBar/FormActionBar.style.ts +11 -0
- package/src/Form/components/FormActionBar/FormActionBar.tsx +63 -0
- package/src/Form/components/FormActionBar/index.ts +2 -0
- package/src/Form/components/FormBody/FormBody.style.ts +27 -0
- package/src/Form/components/FormBody/FormBody.tsx +62 -0
- package/src/Form/components/FormBody/index.ts +1 -0
- package/src/Form/components/FormCache/FormCache.tsx +50 -0
- package/src/Form/components/FormErrorBanner/FormErrorBanner.test.tsx +124 -0
- package/src/Form/components/FormErrorBanner/FormErrorBanner.tsx +34 -0
- package/src/Form/components/FormErrorBanner/index.ts +1 -0
- package/src/Form/components/FormErrorBanner/messages.ts +14 -0
- package/src/Form/components/FormMask/FormMask.style.tsx +16 -0
- package/src/Form/components/FormMask/FormMask.tsx +19 -0
- package/src/Form/components/FormMask/index.ts +1 -0
- package/src/Form/components/FormMessage/FormMessage.test.tsx +72 -0
- package/src/Form/components/FormMessage/FormMessage.tsx +63 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.ts +18 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.tsx +55 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/index.ts +1 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/messages.ts +10 -0
- package/src/Form/components/FormMessage/index.ts +1 -0
- package/src/Form/components/FormMessageBanner/FormMessageBanner.test.tsx +27 -0
- package/src/Form/components/FormMessageBanner/FormMessageBanner.tsx +33 -0
- package/src/Form/components/FormMessageBanner/index.ts +1 -0
- package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +159 -0
- package/src/Form/components/FormSaveButton/FormSaveButton.tsx +103 -0
- package/src/Form/components/FormSaveButton/index.ts +1 -0
- package/src/Form/components/FormSaveButton/messages.ts +9 -0
- package/src/Form/constants.ts +2 -0
- package/src/Form/context/AtlantisFormContext.test.tsx +45 -0
- package/src/Form/context/AtlantisFormContext.tsx +21 -0
- package/src/Form/context/index.ts +5 -0
- package/src/Form/context/types.ts +34 -0
- package/src/Form/hooks/useFormViewRefs.ts +23 -0
- package/src/Form/hooks/useInternalForm.ts +99 -0
- package/src/Form/hooks/useOfflineHandler.ts +36 -0
- package/src/Form/hooks/useSaveButtonPosition.ts +52 -0
- package/src/Form/hooks/useScreenInformation.ts +25 -0
- package/src/Form/hooks/useScrollToError/index.ts +1 -0
- package/src/Form/hooks/useScrollToError/useScrollToError.test.tsx +103 -0
- package/src/Form/hooks/useScrollToError/useScrollToError.ts +102 -0
- package/src/Form/index.ts +13 -0
- package/src/Form/messages.ts +33 -0
- package/src/Form/types.ts +255 -0
- package/src/InputDate/InputDate.test.tsx +295 -0
- package/src/InputDate/InputDate.tsx +231 -0
- package/src/InputDate/index.ts +1 -0
- package/src/InputDate/messages.ts +9 -0
- package/src/InputNumber/InputNumber.tsx +1 -1
- package/src/Menu/Menu.style.ts +16 -0
- package/src/Menu/Menu.test.tsx +201 -0
- package/src/Menu/Menu.tsx +116 -0
- package/src/Menu/components/MenuOption/MenuOption.style.tsx +11 -0
- package/src/Menu/components/MenuOption/MenuOption.tsx +63 -0
- package/src/Menu/components/MenuOption/index.ts +1 -0
- package/src/Menu/components/Overlay/Overlay.style.ts +13 -0
- package/src/Menu/components/Overlay/Overlay.tsx +16 -0
- package/src/Menu/components/Overlay/index.ts +1 -0
- package/src/Menu/index.ts +6 -0
- package/src/Menu/messages.ts +9 -0
- package/src/Menu/types.ts +25 -0
- package/src/Menu/utils.ts +151 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import DateTimePicker from "react-native-modal-datetime-picker";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { FieldError, UseControllerProps } from "react-hook-form";
|
|
5
|
+
import { XOR } from "ts-xor";
|
|
6
|
+
import { useIntl } from "react-intl";
|
|
7
|
+
import { utcToZonedTime } from "date-fns-tz";
|
|
8
|
+
import { format as formatDate } from "date-fns";
|
|
9
|
+
import { messages } from "./messages";
|
|
10
|
+
import { Clearable, InputFieldWrapperProps } from "../InputFieldWrapper";
|
|
11
|
+
import { FormField } from "../FormField";
|
|
12
|
+
import { InputPressable } from "../InputPressable";
|
|
13
|
+
import { useAtlantisContext } from "../AtlantisContext";
|
|
14
|
+
|
|
15
|
+
interface BaseInputDateProps
|
|
16
|
+
extends Pick<InputFieldWrapperProps, "invalid" | "disabled" | "placeholder"> {
|
|
17
|
+
/**
|
|
18
|
+
* Defaulted to "always" so user can clear the dates whenever there's a value.
|
|
19
|
+
*/
|
|
20
|
+
readonly clearable?: Extract<Clearable, "always" | "never">;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* This label is shown to the user when there's no selected date.
|
|
24
|
+
*/
|
|
25
|
+
readonly emptyValueLabel?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maximum date the user can set.
|
|
29
|
+
*/
|
|
30
|
+
readonly maxDate?: Date;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Minimum date the user can set
|
|
34
|
+
*/
|
|
35
|
+
readonly minDate?: Date;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* VoiceOver will read this string when a user selects the element
|
|
39
|
+
*/
|
|
40
|
+
readonly accessibilityLabel?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Helps users understand what will happen when they perform an action
|
|
44
|
+
*/
|
|
45
|
+
readonly accessibilityHint?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface FormControlledInputDate extends BaseInputDateProps {
|
|
49
|
+
/**
|
|
50
|
+
* Adding a `name` would make this component "Form controlled" and must be
|
|
51
|
+
* nested within a `<Form />` component.
|
|
52
|
+
*
|
|
53
|
+
* Cannot be declared if `value` prop is used.
|
|
54
|
+
*/
|
|
55
|
+
readonly name: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Shows an error message below the field and highlights it red when the
|
|
59
|
+
* value is invalid. Only applies when nested within a `<Form />` component.
|
|
60
|
+
*
|
|
61
|
+
* You can see **most** of the rules you can pass in
|
|
62
|
+
* [React-hook-form Documentation](https://react-hook-form.com/api/useform/register#options).
|
|
63
|
+
*/
|
|
64
|
+
readonly validations?: UseControllerProps["rules"];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The initial value for the input.
|
|
68
|
+
*/
|
|
69
|
+
readonly defaultValue?: Date;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The callback that fires whenever a date gets selected.
|
|
73
|
+
*/
|
|
74
|
+
readonly onChange?: (value?: Date | null) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface DevControlledInputDate extends BaseInputDateProps {
|
|
78
|
+
/**
|
|
79
|
+
* The value shown on the field. This gets automatically formatted to the
|
|
80
|
+
* account's date format.
|
|
81
|
+
*
|
|
82
|
+
* Cannot be declared if `name` prop is used.
|
|
83
|
+
*/
|
|
84
|
+
readonly value: Date | string | undefined;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The callback that fires whenever a date gets selected.
|
|
88
|
+
*/
|
|
89
|
+
readonly onChange: (value?: Date) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type InputDateProps = XOR<
|
|
93
|
+
FormControlledInputDate,
|
|
94
|
+
DevControlledInputDate
|
|
95
|
+
>;
|
|
96
|
+
|
|
97
|
+
function formatInvalidState(
|
|
98
|
+
error: FieldError | undefined,
|
|
99
|
+
invalid: InputFieldWrapperProps["invalid"],
|
|
100
|
+
): boolean | string {
|
|
101
|
+
if (invalid) return invalid;
|
|
102
|
+
|
|
103
|
+
if (error && error.message) {
|
|
104
|
+
return error.message;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return Boolean(error);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const display = Platform.OS === "ios" ? "inline" : "default";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Allow users to select a date using the device date picker.
|
|
114
|
+
*/
|
|
115
|
+
export function InputDate(props: InputDateProps): JSX.Element {
|
|
116
|
+
if (props.name) {
|
|
117
|
+
return (
|
|
118
|
+
<FormField<Date>
|
|
119
|
+
name={props.name}
|
|
120
|
+
defaultValue={props.defaultValue}
|
|
121
|
+
validations={props.validations}
|
|
122
|
+
>
|
|
123
|
+
{({ value, onChange, onBlur }, error) => (
|
|
124
|
+
<InternalInputDate
|
|
125
|
+
{...props}
|
|
126
|
+
value={value}
|
|
127
|
+
onChange={(newValue?: Date | null) => {
|
|
128
|
+
onChange(newValue);
|
|
129
|
+
onBlur();
|
|
130
|
+
props.onChange?.(newValue);
|
|
131
|
+
}}
|
|
132
|
+
invalid={formatInvalidState(error, props.invalid)}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
</FormField>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return <InternalInputDate {...props} />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function InternalInputDate({
|
|
143
|
+
clearable = "always",
|
|
144
|
+
disabled,
|
|
145
|
+
emptyValueLabel,
|
|
146
|
+
invalid,
|
|
147
|
+
maxDate,
|
|
148
|
+
minDate,
|
|
149
|
+
placeholder,
|
|
150
|
+
value,
|
|
151
|
+
name,
|
|
152
|
+
onChange,
|
|
153
|
+
accessibilityLabel,
|
|
154
|
+
accessibilityHint,
|
|
155
|
+
}: InputDateProps): JSX.Element {
|
|
156
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
157
|
+
const { formatMessage } = useIntl();
|
|
158
|
+
const { timeZone, dateFormat } = useAtlantisContext();
|
|
159
|
+
|
|
160
|
+
const date = useMemo(() => {
|
|
161
|
+
if (typeof value === "string") return new Date(value);
|
|
162
|
+
return value;
|
|
163
|
+
}, [value]);
|
|
164
|
+
|
|
165
|
+
const formattedDate = useMemo(() => {
|
|
166
|
+
if (date) {
|
|
167
|
+
const zonedTime = utcToZonedTime(date, timeZone);
|
|
168
|
+
return formatDate(zonedTime, dateFormat);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return emptyValueLabel;
|
|
172
|
+
}, [date, emptyValueLabel, timeZone, dateFormat]);
|
|
173
|
+
|
|
174
|
+
const canClearDate = formattedDate === emptyValueLabel ? "never" : clearable;
|
|
175
|
+
|
|
176
|
+
const placeholderLabel =
|
|
177
|
+
placeholder ?? formatMessage(messages.datePlaceholder);
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
<InputPressable
|
|
182
|
+
clearable={canClearDate}
|
|
183
|
+
disabled={disabled}
|
|
184
|
+
invalid={invalid}
|
|
185
|
+
placeholder={placeholderLabel}
|
|
186
|
+
prefix={{ icon: "calendar" }}
|
|
187
|
+
value={formattedDate}
|
|
188
|
+
onClear={handleClear}
|
|
189
|
+
onPress={showDatePicker}
|
|
190
|
+
accessibilityLabel={accessibilityLabel}
|
|
191
|
+
accessibilityHint={accessibilityHint}
|
|
192
|
+
/>
|
|
193
|
+
<DateTimePicker
|
|
194
|
+
testID="inputDate-datePicker"
|
|
195
|
+
date={date || undefined}
|
|
196
|
+
display={display}
|
|
197
|
+
isVisible={showPicker}
|
|
198
|
+
maximumDate={maxDate}
|
|
199
|
+
minimumDate={minDate}
|
|
200
|
+
mode="date"
|
|
201
|
+
onCancel={handleCancel}
|
|
202
|
+
onConfirm={handleConfirm}
|
|
203
|
+
/>
|
|
204
|
+
</>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
function showDatePicker() {
|
|
208
|
+
setShowPicker(true);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleConfirm(newVal: Date) {
|
|
212
|
+
setShowPicker(false);
|
|
213
|
+
onChange?.(newVal);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function handleCancel() {
|
|
217
|
+
setShowPicker(false);
|
|
218
|
+
|
|
219
|
+
// Ensure a change happens so we trigger the validation of one exists
|
|
220
|
+
onChange?.(date);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function handleClear() {
|
|
224
|
+
// Returns null only for Form controlled scenarios due to a limitation of react-hook-form that doesn't allow passing undefined to form values.
|
|
225
|
+
if (name) {
|
|
226
|
+
onChange?.(null);
|
|
227
|
+
} else {
|
|
228
|
+
onChange?.(undefined);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InputDate, InputDateProps } from "./InputDate";
|
|
@@ -14,7 +14,7 @@ export interface InputNumberProps
|
|
|
14
14
|
> {
|
|
15
15
|
readonly value?: number;
|
|
16
16
|
readonly defaultValue?: number;
|
|
17
|
-
readonly onChange
|
|
17
|
+
readonly onChange?: (newValue?: number | string | undefined) => void;
|
|
18
18
|
readonly keyboard?: NumberKeyboard;
|
|
19
19
|
/**
|
|
20
20
|
* Used to locate this view in end-to-end tests
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../utils/design";
|
|
3
|
+
|
|
4
|
+
const menuWidth = 208;
|
|
5
|
+
|
|
6
|
+
export const styles = StyleSheet.create({
|
|
7
|
+
menu: {
|
|
8
|
+
position: "absolute",
|
|
9
|
+
backgroundColor: tokens["color-surface"],
|
|
10
|
+
paddingHorizontal: tokens["space-small"],
|
|
11
|
+
paddingVertical: tokens["space-small"] + tokens["space-smallest"],
|
|
12
|
+
borderRadius: tokens["radius-larger"],
|
|
13
|
+
width: menuWidth,
|
|
14
|
+
...tokens["shadow-high"],
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { Host } from "react-native-portalize";
|
|
4
|
+
import { View } from "react-native";
|
|
5
|
+
import { tokens } from "@jobber/design/foundation";
|
|
6
|
+
import { Menu, MenuOptionProps, MenuProps } from ".";
|
|
7
|
+
import { messages } from "./messages";
|
|
8
|
+
import { Icon } from "../Icon";
|
|
9
|
+
import { Button } from "../Button";
|
|
10
|
+
|
|
11
|
+
const mockOnPress = jest.fn();
|
|
12
|
+
jest
|
|
13
|
+
.spyOn(View.prototype, "measureInWindow")
|
|
14
|
+
.mockImplementation(cb => cb(50, 50, 100, 100));
|
|
15
|
+
|
|
16
|
+
const setup = (props?: MenuProps) => {
|
|
17
|
+
return render(
|
|
18
|
+
<Host>
|
|
19
|
+
<Menu
|
|
20
|
+
menuOptions={props?.menuOptions}
|
|
21
|
+
customActivator={props?.customActivator}
|
|
22
|
+
/>
|
|
23
|
+
</Host>,
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("Menu", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
mockOnPress.mockClear();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders the default Activator", () => {
|
|
33
|
+
const { getByTestId, getByLabelText } = setup({
|
|
34
|
+
menuOptions: [{ label: "hi", icon: "add", onPress: mockOnPress }],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(getByTestId("more")).toBeDefined();
|
|
38
|
+
expect(getByLabelText(messages.more.defaultMessage)).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders every menu option when menu is opened", () => {
|
|
42
|
+
const menuOptions: MenuOptionProps[] = [
|
|
43
|
+
{ label: "option1", icon: "add", onPress: mockOnPress },
|
|
44
|
+
{ label: "option2", icon: "arrowDown", onPress: mockOnPress },
|
|
45
|
+
{ label: "option3", onPress: mockOnPress },
|
|
46
|
+
];
|
|
47
|
+
const { getByLabelText } = setup({
|
|
48
|
+
menuOptions,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
52
|
+
expect(getByLabelText(menuOptions[0].label)).toBeDefined();
|
|
53
|
+
expect(getByLabelText(menuOptions[1].label)).toBeDefined();
|
|
54
|
+
expect(getByLabelText(menuOptions[2].label)).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("Custom Activator", () => {
|
|
58
|
+
it("renders a custom Activator", () => {
|
|
59
|
+
const { getByTestId } = setup({
|
|
60
|
+
menuOptions: [{ label: "hi", onPress: mockOnPress }],
|
|
61
|
+
customActivator: <Icon name="addNote" />,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(getByTestId("addNote")).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("renders menu when the custom activator is clicked", () => {
|
|
68
|
+
const { getByLabelText, getByTestId, getAllByTestId } = setup({
|
|
69
|
+
menuOptions: [
|
|
70
|
+
{ label: "hi", icon: "add", onPress: mockOnPress },
|
|
71
|
+
{ label: "option2", onPress: mockOnPress },
|
|
72
|
+
],
|
|
73
|
+
customActivator: <Icon name="addNote" />,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
fireEvent.press(getByTestId("addNote"));
|
|
77
|
+
|
|
78
|
+
expect(getAllByTestId("ATL-MENU-OPTIONS")).toHaveLength(2);
|
|
79
|
+
expect(getByTestId("add")).toBeDefined();
|
|
80
|
+
expect(getByLabelText("hi")).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("Pressable used as Custom Activator", () => {
|
|
84
|
+
it("menu is open and custom activator's onPress is called", () => {
|
|
85
|
+
const mockActivatorPress = jest.fn();
|
|
86
|
+
const buttonLabel = "Test me!";
|
|
87
|
+
const menuOptions: MenuOptionProps[] = [
|
|
88
|
+
{ label: "menuOption", icon: "add", onPress: mockOnPress },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const { getByLabelText } = setup({
|
|
92
|
+
menuOptions,
|
|
93
|
+
customActivator: (
|
|
94
|
+
<Button label={buttonLabel} onPress={mockActivatorPress} />
|
|
95
|
+
),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
fireEvent.press(getByLabelText(buttonLabel));
|
|
99
|
+
|
|
100
|
+
expect(mockActivatorPress).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(getByLabelText(menuOptions[0].label)).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("Menu Options", () => {
|
|
107
|
+
it("fires the onPress of the menu option", () => {
|
|
108
|
+
const { getByLabelText } = setup({
|
|
109
|
+
menuOptions: [
|
|
110
|
+
{
|
|
111
|
+
label: "hi",
|
|
112
|
+
icon: "add",
|
|
113
|
+
onPress: mockOnPress,
|
|
114
|
+
destructive: true,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
120
|
+
fireEvent.press(getByLabelText("hi"));
|
|
121
|
+
expect(mockOnPress).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("renders a menuOption with an icon with destructive styling", () => {
|
|
125
|
+
const { getByLabelText, getByTestId } = setup({
|
|
126
|
+
menuOptions: [
|
|
127
|
+
{ label: "hi", icon: "add", onPress: mockOnPress, destructive: true },
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
132
|
+
expect(getByTestId("add").props.style).toContainEqual({
|
|
133
|
+
display: "flex",
|
|
134
|
+
fill: tokens["color-critical"],
|
|
135
|
+
height: 24,
|
|
136
|
+
verticalAlign: "middle",
|
|
137
|
+
width: 24,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("closes the menu after clicking on a menu option", () => {
|
|
142
|
+
const { getByLabelText, queryByLabelText } = setup({
|
|
143
|
+
menuOptions: [
|
|
144
|
+
{
|
|
145
|
+
label: "hi",
|
|
146
|
+
icon: "add",
|
|
147
|
+
onPress: mockOnPress,
|
|
148
|
+
destructive: true,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
154
|
+
fireEvent.press(getByLabelText("hi"));
|
|
155
|
+
expect(mockOnPress).toHaveBeenCalled();
|
|
156
|
+
expect(queryByLabelText("hi")).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("transforms the text", () => {
|
|
160
|
+
const { getByLabelText, getByText } = setup({
|
|
161
|
+
menuOptions: [
|
|
162
|
+
{
|
|
163
|
+
label: "hi",
|
|
164
|
+
icon: "add",
|
|
165
|
+
onPress: mockOnPress,
|
|
166
|
+
textTransform: "capitalize",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
171
|
+
expect(getByText("Hi")).toBeDefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("does not transform the text when textTransform set to none", () => {
|
|
175
|
+
const { getByLabelText, getByText } = setup({
|
|
176
|
+
menuOptions: [
|
|
177
|
+
{
|
|
178
|
+
label: "hi",
|
|
179
|
+
icon: "add",
|
|
180
|
+
onPress: mockOnPress,
|
|
181
|
+
textTransform: "none",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
186
|
+
expect(getByText("hi")).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("renders a menu when the default activator is clicked", () => {
|
|
191
|
+
const { getByLabelText, getByTestId } = setup({
|
|
192
|
+
menuOptions: [{ label: "hi", icon: "add", onPress: mockOnPress }],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
196
|
+
|
|
197
|
+
expect(getByTestId("ATL-MENU-OPTIONS")).toBeDefined();
|
|
198
|
+
expect(getByTestId("add")).toBeDefined();
|
|
199
|
+
expect(getByLabelText("hi")).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
LayoutRectangle,
|
|
4
|
+
Pressable,
|
|
5
|
+
View,
|
|
6
|
+
useWindowDimensions,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { Portal } from "react-native-portalize";
|
|
9
|
+
import { useIntl } from "react-intl";
|
|
10
|
+
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
|
11
|
+
import { styles } from "./Menu.style";
|
|
12
|
+
import { messages } from "./messages";
|
|
13
|
+
import { findViewpoint } from "./utils";
|
|
14
|
+
import { MenuProps } from "./types";
|
|
15
|
+
import { MenuOption } from "./components/MenuOption";
|
|
16
|
+
import { Overlay } from "./components/Overlay";
|
|
17
|
+
import { tokens } from "../utils/design";
|
|
18
|
+
import { Button } from "../Button";
|
|
19
|
+
import { Content } from "../Content";
|
|
20
|
+
import { useAtlantisContext } from "../AtlantisContext";
|
|
21
|
+
|
|
22
|
+
export function Menu({ menuOptions, customActivator }: MenuProps): JSX.Element {
|
|
23
|
+
const [open, setOpen] = useState<boolean>(false);
|
|
24
|
+
const [menuPosition, setMenuPosition] = useState<object>();
|
|
25
|
+
const activatorLayout = useRef<LayoutRectangle>();
|
|
26
|
+
const menuButtonRef = useRef<View | null>();
|
|
27
|
+
const screenInfo = useScreenInformation();
|
|
28
|
+
|
|
29
|
+
const { formatMessage } = useIntl();
|
|
30
|
+
|
|
31
|
+
const findMenuLayout = useCallback(() => {
|
|
32
|
+
if (activatorLayout.current) {
|
|
33
|
+
setMenuPosition(findViewpoint(screenInfo, activatorLayout.current));
|
|
34
|
+
}
|
|
35
|
+
}, [screenInfo, activatorLayout]);
|
|
36
|
+
|
|
37
|
+
const activatorOnPress = (onPress?: () => void) => {
|
|
38
|
+
menuButtonRef.current?.measureInWindow(
|
|
39
|
+
(x: number, y: number, width: number, height: number) => {
|
|
40
|
+
activatorLayout.current = {
|
|
41
|
+
x,
|
|
42
|
+
y,
|
|
43
|
+
width,
|
|
44
|
+
height,
|
|
45
|
+
};
|
|
46
|
+
findMenuLayout();
|
|
47
|
+
setOpen(true);
|
|
48
|
+
onPress && onPress();
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<View
|
|
56
|
+
ref={ref => {
|
|
57
|
+
menuButtonRef.current = ref;
|
|
58
|
+
}}
|
|
59
|
+
collapsable={false}
|
|
60
|
+
>
|
|
61
|
+
{customActivator && (
|
|
62
|
+
<Pressable
|
|
63
|
+
style={({ pressed }) => [
|
|
64
|
+
{
|
|
65
|
+
opacity: pressed ? tokens["opacity-pressed"] : 1,
|
|
66
|
+
},
|
|
67
|
+
]}
|
|
68
|
+
pointerEvents="box-only"
|
|
69
|
+
onPress={() => {
|
|
70
|
+
activatorOnPress(customActivator.props.onPress);
|
|
71
|
+
}}
|
|
72
|
+
onLongPress={customActivator.props.onLongPress}
|
|
73
|
+
>
|
|
74
|
+
{customActivator}
|
|
75
|
+
</Pressable>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{!customActivator && (
|
|
79
|
+
<Button
|
|
80
|
+
icon="more"
|
|
81
|
+
accessibilityLabel={formatMessage(messages.more)}
|
|
82
|
+
variation="cancel"
|
|
83
|
+
type="tertiary"
|
|
84
|
+
onPress={() => {
|
|
85
|
+
activatorOnPress();
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</View>
|
|
90
|
+
<Portal>
|
|
91
|
+
{open && (
|
|
92
|
+
<>
|
|
93
|
+
<Overlay setOpen={setOpen} />
|
|
94
|
+
<View style={[open && menuPosition, styles.menu]}>
|
|
95
|
+
<Content spacing="none" childSpacing="small">
|
|
96
|
+
{menuOptions?.map((menuOption, index) => {
|
|
97
|
+
return (
|
|
98
|
+
<MenuOption {...menuOption} key={index} setOpen={setOpen} />
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</Content>
|
|
102
|
+
</View>
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
</Portal>
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function useScreenInformation() {
|
|
111
|
+
const { headerHeight } = useAtlantisContext();
|
|
112
|
+
const windowWidth = useWindowDimensions().width;
|
|
113
|
+
const { height: windowHeight } = useSafeAreaFrame();
|
|
114
|
+
|
|
115
|
+
return { headerHeight, windowWidth, windowHeight };
|
|
116
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../../utils/design";
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
menuOption: {
|
|
6
|
+
display: "flex",
|
|
7
|
+
paddingHorizontal: tokens["space-base"],
|
|
8
|
+
paddingVertical: tokens["space-small"],
|
|
9
|
+
borderRadius: tokens["radius-large"],
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, View } from "react-native";
|
|
3
|
+
import capitalize from "lodash/capitalize";
|
|
4
|
+
import { styles } from "./MenuOption.style";
|
|
5
|
+
import { MenuOptionInternalProps } from "../../types";
|
|
6
|
+
import { tokens } from "../../../utils/design";
|
|
7
|
+
import { Flex } from "../../../Flex";
|
|
8
|
+
import { Typography } from "../../../Typography";
|
|
9
|
+
import { Icon } from "../../../Icon";
|
|
10
|
+
|
|
11
|
+
export function MenuOption({
|
|
12
|
+
label,
|
|
13
|
+
icon,
|
|
14
|
+
iconColor = "heading",
|
|
15
|
+
textAlign,
|
|
16
|
+
destructive,
|
|
17
|
+
textTransform = "capitalize",
|
|
18
|
+
onPress,
|
|
19
|
+
setOpen,
|
|
20
|
+
}: MenuOptionInternalProps): JSX.Element {
|
|
21
|
+
const destructiveColor = "critical";
|
|
22
|
+
const textVariation = destructive ? destructiveColor : "heading";
|
|
23
|
+
return (
|
|
24
|
+
<View testID="ATL-MENU-OPTIONS">
|
|
25
|
+
<Pressable
|
|
26
|
+
style={({ pressed }) => [
|
|
27
|
+
styles.menuOption,
|
|
28
|
+
{ opacity: pressed ? tokens["opacity-pressed"] : 1 },
|
|
29
|
+
]}
|
|
30
|
+
onPress={() => {
|
|
31
|
+
onPress();
|
|
32
|
+
setOpen(false);
|
|
33
|
+
}}
|
|
34
|
+
accessibilityLabel={label}
|
|
35
|
+
>
|
|
36
|
+
<Flex
|
|
37
|
+
template={["grow", "shrink"]}
|
|
38
|
+
align={"flex-start"}
|
|
39
|
+
gap={"smaller"}
|
|
40
|
+
>
|
|
41
|
+
<Typography
|
|
42
|
+
selectable={false}
|
|
43
|
+
color={textVariation}
|
|
44
|
+
fontWeight={"semiBold"}
|
|
45
|
+
lineHeight={"large"}
|
|
46
|
+
align={textAlign}
|
|
47
|
+
>
|
|
48
|
+
{textTransform === "capitalize"
|
|
49
|
+
? capitalize(label.toLocaleLowerCase())
|
|
50
|
+
: label}
|
|
51
|
+
</Typography>
|
|
52
|
+
|
|
53
|
+
{icon && (
|
|
54
|
+
<Icon
|
|
55
|
+
name={icon}
|
|
56
|
+
color={destructive ? destructiveColor : iconColor}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
</Flex>
|
|
60
|
+
</Pressable>
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MenuOption } from "./MenuOption";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Dimensions, StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../../utils/design";
|
|
3
|
+
|
|
4
|
+
const { height } = Dimensions.get("window");
|
|
5
|
+
|
|
6
|
+
export const styles = StyleSheet.create({
|
|
7
|
+
overlay: {
|
|
8
|
+
...StyleSheet.absoluteFillObject,
|
|
9
|
+
backgroundColor: tokens["color-overlay"],
|
|
10
|
+
opacity: 0,
|
|
11
|
+
height,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, View } from "react-native";
|
|
3
|
+
import { styles } from "./Overlay.style";
|
|
4
|
+
import { OverlayProp } from "../../types";
|
|
5
|
+
|
|
6
|
+
export function Overlay({ setOpen }: OverlayProp): JSX.Element {
|
|
7
|
+
return (
|
|
8
|
+
<Pressable
|
|
9
|
+
onPressIn={() => {
|
|
10
|
+
setOpen(false);
|
|
11
|
+
}}
|
|
12
|
+
>
|
|
13
|
+
<View style={styles.overlay} />
|
|
14
|
+
</Pressable>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Overlay } from "./Overlay";
|