@servicetitan/form 14.3.0 → 16.0.2
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/date-range-picker/date-range-picker.d.ts +47 -0
- package/dist/date-range-picker/date-range-picker.d.ts.map +1 -0
- package/dist/date-range-picker/date-range-picker.js +256 -0
- package/dist/date-range-picker/date-range-picker.js.map +1 -0
- package/dist/date-range-picker/date-range-picker.module.css +37 -0
- package/dist/date-range-picker/index.d.ts +2 -0
- package/dist/date-range-picker/index.d.ts.map +1 -0
- package/dist/date-range-picker/index.js +14 -0
- package/dist/date-range-picker/index.js.map +1 -0
- package/dist/demo/date-range-picker.d.ts +3 -0
- package/dist/demo/date-range-picker.d.ts.map +1 -0
- package/dist/demo/date-range-picker.js +17 -0
- package/dist/demo/date-range-picker.js.map +1 -0
- package/dist/demo/index.d.ts +4 -0
- package/dist/demo/index.d.ts.map +1 -1
- package/dist/demo/index.js +4 -0
- package/dist/demo/index.js.map +1 -1
- package/dist/demo/input-date-mask.d.ts +3 -0
- package/dist/demo/input-date-mask.d.ts.map +1 -0
- package/dist/demo/input-date-mask.js +17 -0
- package/dist/demo/input-date-mask.js.map +1 -0
- package/dist/demo/original-number-input.d.ts +3 -0
- package/dist/demo/original-number-input.d.ts.map +1 -0
- package/dist/demo/original-number-input.js +17 -0
- package/dist/demo/original-number-input.js.map +1 -0
- package/dist/demo/phone-number-input.d.ts +3 -0
- package/dist/demo/phone-number-input.d.ts.map +1 -0
- package/dist/demo/phone-number-input.js +8 -0
- package/dist/demo/phone-number-input.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/input-date-mask/index.d.ts +2 -0
- package/dist/input-date-mask/index.d.ts.map +1 -0
- package/dist/input-date-mask/index.js +14 -0
- package/dist/input-date-mask/index.js.map +1 -0
- package/dist/input-date-mask/input-date-mask.d.ts +14 -0
- package/dist/input-date-mask/input-date-mask.d.ts.map +1 -0
- package/dist/input-date-mask/input-date-mask.js +122 -0
- package/dist/input-date-mask/input-date-mask.js.map +1 -0
- package/dist/input-date-mask/input-date-mask.module.css +22 -0
- package/dist/original-number-input/index.d.ts +2 -0
- package/dist/original-number-input/index.d.ts.map +1 -0
- package/dist/original-number-input/index.js +14 -0
- package/dist/original-number-input/index.js.map +1 -0
- package/dist/original-number-input/ordinal-number-input.d.ts +22 -0
- package/dist/original-number-input/ordinal-number-input.d.ts.map +1 -0
- package/dist/original-number-input/ordinal-number-input.js +149 -0
- package/dist/original-number-input/ordinal-number-input.js.map +1 -0
- package/dist/phone-number-input/index.d.ts +2 -0
- package/dist/phone-number-input/index.d.ts.map +1 -0
- package/dist/phone-number-input/index.js +14 -0
- package/dist/phone-number-input/index.js.map +1 -0
- package/dist/phone-number-input/phone-number-input.d.ts +7 -0
- package/dist/phone-number-input/phone-number-input.d.ts.map +1 -0
- package/dist/phone-number-input/phone-number-input.js +26 -0
- package/dist/phone-number-input/phone-number-input.js.map +1 -0
- package/package.json +20 -13
- package/src/date-range-picker/date-range-picker.module.css +37 -0
- package/src/date-range-picker/date-range-picker.module.css.d.ts +7 -0
- package/src/date-range-picker/date-range-picker.tsx +297 -0
- package/src/date-range-picker/index.ts +1 -0
- package/src/demo/date-range-picker.tsx +33 -0
- package/src/demo/index.ts +4 -0
- package/src/demo/input-date-mask.tsx +30 -0
- package/src/demo/original-number-input.tsx +32 -0
- package/src/demo/phone-number-input.tsx +9 -0
- package/src/index.ts +4 -0
- package/src/input-date-mask/index.ts +1 -0
- package/src/input-date-mask/input-date-mask.module.css +22 -0
- package/src/input-date-mask/input-date-mask.module.css.d.ts +4 -0
- package/src/input-date-mask/input-date-mask.tsx +157 -0
- package/src/original-number-input/__tests__/ordinal-number-input.test.tsx +51 -0
- package/src/original-number-input/index.ts +1 -0
- package/src/original-number-input/ordinal-number-input.tsx +111 -0
- package/src/phone-number-input/index.ts +1 -0
- package/src/phone-number-input/phone-number-input.tsx +19 -0
@@ -0,0 +1,297 @@
|
|
1
|
+
import { ComponentType, Component, SyntheticEvent } from 'react';
|
2
|
+
import {
|
3
|
+
DateRangePicker as DateRangePickerKendo,
|
4
|
+
DateRangePickerChangeEvent,
|
5
|
+
CalendarHeaderTitleProps,
|
6
|
+
MultiViewCalendarProps,
|
7
|
+
CalendarCellProps,
|
8
|
+
ActiveView,
|
9
|
+
} from '@progress/kendo-react-dateinputs';
|
10
|
+
import { Icon, InputDateMask, Stack } from '@servicetitan/design-system';
|
11
|
+
import { observable, action, makeObservable } from 'mobx';
|
12
|
+
import { observer } from 'mobx-react';
|
13
|
+
import classnames from 'classnames';
|
14
|
+
import moment from 'moment';
|
15
|
+
import * as Styles from './date-range-picker.module.css';
|
16
|
+
import { FieldState } from 'formstate';
|
17
|
+
import { DateRange } from '../date-range';
|
18
|
+
|
19
|
+
export interface DateRangePickerProps {
|
20
|
+
field: FieldState<DateRange | undefined>;
|
21
|
+
['qa-testing']: string;
|
22
|
+
placeHolder?: string;
|
23
|
+
minDate?: Date;
|
24
|
+
maxDate?: Date;
|
25
|
+
calendarMinDate?: Date;
|
26
|
+
calendarMaxDate?: Date;
|
27
|
+
topView?: ActiveView;
|
28
|
+
bottomView?: ActiveView;
|
29
|
+
className?: string;
|
30
|
+
calendarComponent?: ComponentType<MultiViewCalendarProps>;
|
31
|
+
headerTitleComponent?: ComponentType<CalendarHeaderTitleProps>;
|
32
|
+
cellComponent?: ComponentType<CalendarCellProps>;
|
33
|
+
inputFormat?: string;
|
34
|
+
horizontalAlign?: 'left' | 'center' | 'right';
|
35
|
+
disabled?: boolean;
|
36
|
+
small?: boolean;
|
37
|
+
disableRangeValidation?: boolean;
|
38
|
+
maskChar?: string;
|
39
|
+
}
|
40
|
+
|
41
|
+
@observer
|
42
|
+
export class DateRangePicker extends Component<DateRangePickerProps> {
|
43
|
+
// `HTMLDivElement` leads to issues with Node/SSR
|
44
|
+
@observable wrapElem?: any /* HTMLDivElement */;
|
45
|
+
isFocused = false;
|
46
|
+
@observable showDatePicker = false;
|
47
|
+
isIconClicked = false;
|
48
|
+
|
49
|
+
constructor(props: DateRangePickerProps) {
|
50
|
+
super(props);
|
51
|
+
makeObservable(this);
|
52
|
+
}
|
53
|
+
|
54
|
+
@action
|
55
|
+
handleRef = (wrapElem: HTMLDivElement) => {
|
56
|
+
this.wrapElem = wrapElem;
|
57
|
+
};
|
58
|
+
|
59
|
+
@action
|
60
|
+
toggleShow(show?: boolean) {
|
61
|
+
this.showDatePicker = show !== undefined ? show : !this.showDatePicker;
|
62
|
+
}
|
63
|
+
|
64
|
+
handleFocus = (evt: Event) => {
|
65
|
+
if (!(evt.target instanceof Node)) {
|
66
|
+
return;
|
67
|
+
}
|
68
|
+
|
69
|
+
const isPrevFocused = this.isFocused;
|
70
|
+
|
71
|
+
this.isFocused = this.wrapElem!.contains(evt.target);
|
72
|
+
|
73
|
+
if (!this.isFocused && isPrevFocused) {
|
74
|
+
// click outside
|
75
|
+
this.props.field.enableAutoValidationAndValidate();
|
76
|
+
|
77
|
+
this.toggleShow(false);
|
78
|
+
} else if (this.isFocused && !isPrevFocused && !this.isIconClicked) {
|
79
|
+
// click inside but outside icons
|
80
|
+
this.props.field.disableAutoValidation();
|
81
|
+
}
|
82
|
+
|
83
|
+
this.isIconClicked = false;
|
84
|
+
};
|
85
|
+
|
86
|
+
handleClickIcon = () => {
|
87
|
+
this.isIconClicked = true;
|
88
|
+
|
89
|
+
if (this.showDatePicker) {
|
90
|
+
this.props.field.enableAutoValidationAndValidate();
|
91
|
+
} else {
|
92
|
+
this.props.field.disableAutoValidation();
|
93
|
+
}
|
94
|
+
|
95
|
+
this.toggleShow();
|
96
|
+
};
|
97
|
+
|
98
|
+
componentDidMount() {
|
99
|
+
window.addEventListener('focus', this.handleFocus, true);
|
100
|
+
window.addEventListener('click', this.handleFocus);
|
101
|
+
}
|
102
|
+
|
103
|
+
componentWillUnmount() {
|
104
|
+
window.removeEventListener('focus', this.handleFocus, true);
|
105
|
+
window.removeEventListener('click', this.handleFocus);
|
106
|
+
}
|
107
|
+
|
108
|
+
handleChange = (event: DateRangePickerChangeEvent) => {
|
109
|
+
const value = event.target.value;
|
110
|
+
|
111
|
+
if (value.start && value.end && value.start > value.end) {
|
112
|
+
[value.start, value.end] = [value.end, value.start];
|
113
|
+
}
|
114
|
+
|
115
|
+
this.props.field.onChange({ from: value.start ?? undefined, to: value.end ?? undefined });
|
116
|
+
};
|
117
|
+
|
118
|
+
handleFromChange = (from?: Date) => {
|
119
|
+
const to = this.props.field.value ? this.props.field.value.to : undefined;
|
120
|
+
|
121
|
+
if (from || to) {
|
122
|
+
this.props.field.onChange({ from, to });
|
123
|
+
} else {
|
124
|
+
this.props.field.onChange(undefined);
|
125
|
+
}
|
126
|
+
};
|
127
|
+
|
128
|
+
handleToChange = (to?: Date) => {
|
129
|
+
const from = this.props.field.value ? this.props.field.value.from : undefined;
|
130
|
+
|
131
|
+
if (from || to) {
|
132
|
+
this.props.field.onChange({ from, to });
|
133
|
+
} else {
|
134
|
+
this.props.field.onChange(undefined);
|
135
|
+
}
|
136
|
+
};
|
137
|
+
|
138
|
+
render() {
|
139
|
+
const { hasError } = this.props.field;
|
140
|
+
const {
|
141
|
+
'qa-testing': qaTestingLocator,
|
142
|
+
placeHolder,
|
143
|
+
inputFormat,
|
144
|
+
className,
|
145
|
+
calendarComponent,
|
146
|
+
headerTitleComponent,
|
147
|
+
cellComponent,
|
148
|
+
minDate,
|
149
|
+
maxDate,
|
150
|
+
calendarMinDate,
|
151
|
+
calendarMaxDate,
|
152
|
+
topView,
|
153
|
+
bottomView,
|
154
|
+
horizontalAlign,
|
155
|
+
disabled = false,
|
156
|
+
small,
|
157
|
+
maskChar,
|
158
|
+
} = this.props;
|
159
|
+
|
160
|
+
const value = this.props.field.value ? this.props.field.value : {};
|
161
|
+
|
162
|
+
const horizontalAlignClass = classnames({
|
163
|
+
[Styles.popupCenter]: horizontalAlign === 'center',
|
164
|
+
[Styles.popupRight]: horizontalAlign === 'right',
|
165
|
+
});
|
166
|
+
|
167
|
+
return (
|
168
|
+
<div className={classnames(Styles.dateRangePicker, className)} ref={this.handleRef}>
|
169
|
+
<DateRangePickerKendo
|
170
|
+
className={Styles.dateRangePickerKendo}
|
171
|
+
calendarSettings={{
|
172
|
+
min: minDate ?? calendarMinDate,
|
173
|
+
max: maxDate ?? calendarMaxDate,
|
174
|
+
topView,
|
175
|
+
bottomView,
|
176
|
+
disabled,
|
177
|
+
mode: 'range',
|
178
|
+
headerTitle: headerTitleComponent,
|
179
|
+
cell: cellComponent,
|
180
|
+
}}
|
181
|
+
popupSettings={{
|
182
|
+
appendTo: this.wrapElem,
|
183
|
+
className: horizontalAlignClass,
|
184
|
+
}}
|
185
|
+
startDateInputSettings={{
|
186
|
+
tabIndex: -1,
|
187
|
+
}}
|
188
|
+
endDateInputSettings={{
|
189
|
+
tabIndex: -1,
|
190
|
+
}}
|
191
|
+
show={this.showDatePicker}
|
192
|
+
value={{ start: value.from ?? null, end: value.to ?? null }}
|
193
|
+
onChange={this.handleChange}
|
194
|
+
min={minDate}
|
195
|
+
max={maxDate}
|
196
|
+
calendar={calendarComponent}
|
197
|
+
/>
|
198
|
+
<Stack alignItems="center" className="flex-grow-1">
|
199
|
+
<Stack.Item fill>
|
200
|
+
<InputDateMask
|
201
|
+
shortLabel={
|
202
|
+
<Icon
|
203
|
+
name="today"
|
204
|
+
className={classnames(
|
205
|
+
'cursor-pointer',
|
206
|
+
`${qaTestingLocator}-icon-from`
|
207
|
+
)}
|
208
|
+
onClick={this.handleClickIcon}
|
209
|
+
/>
|
210
|
+
}
|
211
|
+
placeholder={placeHolder}
|
212
|
+
value={value.from}
|
213
|
+
onChange={this.handleFromChange}
|
214
|
+
error={hasError}
|
215
|
+
disabled={disabled}
|
216
|
+
qa-testing={`${qaTestingLocator}-from`}
|
217
|
+
dateFormat={inputFormat}
|
218
|
+
minDate={minDate}
|
219
|
+
maxDate={this.maxFromDate}
|
220
|
+
small={small}
|
221
|
+
maskChar={maskChar}
|
222
|
+
/>
|
223
|
+
</Stack.Item>
|
224
|
+
<Icon name="arrow_forward" className={Styles.endLabel} />
|
225
|
+
<Stack.Item fill>
|
226
|
+
<InputDateMask
|
227
|
+
shortLabel={
|
228
|
+
<Icon
|
229
|
+
name="today"
|
230
|
+
className={classnames(
|
231
|
+
'cursor-pointer',
|
232
|
+
`${qaTestingLocator}-icon-to`
|
233
|
+
)}
|
234
|
+
onClick={this.handleClickIcon}
|
235
|
+
/>
|
236
|
+
}
|
237
|
+
placeholder={placeHolder}
|
238
|
+
value={value.to}
|
239
|
+
onChange={this.handleToChange}
|
240
|
+
error={hasError}
|
241
|
+
disabled={disabled}
|
242
|
+
qa-testing={`${qaTestingLocator}-to`}
|
243
|
+
dateFormat={inputFormat}
|
244
|
+
minDate={this.minToDate}
|
245
|
+
maxDate={maxDate}
|
246
|
+
small={small}
|
247
|
+
maskChar={maskChar}
|
248
|
+
/>
|
249
|
+
</Stack.Item>
|
250
|
+
</Stack>
|
251
|
+
<input
|
252
|
+
hidden
|
253
|
+
qa-testing={qaTestingLocator}
|
254
|
+
onChange={this.handleHiddenInputChange}
|
255
|
+
/>
|
256
|
+
</div>
|
257
|
+
);
|
258
|
+
}
|
259
|
+
|
260
|
+
formatDate(date?: Date, inputFormat = 'L') {
|
261
|
+
return date ? moment(date).format(inputFormat) : '';
|
262
|
+
}
|
263
|
+
|
264
|
+
handleHiddenInputChange = (ev: SyntheticEvent<HTMLInputElement>) => {
|
265
|
+
const values = ev.currentTarget.value.split('-').map(v => new Date(v));
|
266
|
+
this.props.field.onChange({ from: values[0] || undefined, to: values[1] || undefined });
|
267
|
+
};
|
268
|
+
|
269
|
+
get maxFromDate() {
|
270
|
+
const { disableRangeValidation, maxDate, field } = this.props;
|
271
|
+
if (disableRangeValidation) {
|
272
|
+
return maxDate;
|
273
|
+
}
|
274
|
+
|
275
|
+
const to = field.value ? field.value.to : undefined;
|
276
|
+
|
277
|
+
if (maxDate && to) {
|
278
|
+
return maxDate > to ? to : maxDate;
|
279
|
+
}
|
280
|
+
|
281
|
+
return maxDate ?? to;
|
282
|
+
}
|
283
|
+
|
284
|
+
get minToDate() {
|
285
|
+
const { disableRangeValidation, minDate, field } = this.props;
|
286
|
+
if (disableRangeValidation) {
|
287
|
+
return minDate;
|
288
|
+
}
|
289
|
+
|
290
|
+
const from = field.value ? field.value.from : undefined;
|
291
|
+
if (minDate && from) {
|
292
|
+
return minDate < from ? from : minDate;
|
293
|
+
}
|
294
|
+
|
295
|
+
return minDate ?? from;
|
296
|
+
}
|
297
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './date-range-picker';
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import { Fragment, useRef, FC } from 'react';
|
2
|
+
|
3
|
+
import { observer } from 'mobx-react';
|
4
|
+
|
5
|
+
import { Text } from '@servicetitan/design-system';
|
6
|
+
|
7
|
+
import { DateRangePicker } from '..';
|
8
|
+
|
9
|
+
import { FieldState } from 'formstate';
|
10
|
+
import { DateRange } from '../date-range';
|
11
|
+
|
12
|
+
function useDateRangeField<T extends DateRange | undefined>(initial: T) {
|
13
|
+
return useRef(new FieldState(initial)).current;
|
14
|
+
}
|
15
|
+
|
16
|
+
export const DateRangePickerExample: FC = observer(() => {
|
17
|
+
const defaultField = useDateRangeField<DateRange | undefined>(undefined);
|
18
|
+
|
19
|
+
return (
|
20
|
+
<Fragment>
|
21
|
+
<Text size={4} className="m-b-half">
|
22
|
+
Default
|
23
|
+
</Text>
|
24
|
+
<DateRangePicker
|
25
|
+
maskChar="X"
|
26
|
+
field={defaultField}
|
27
|
+
qa-testing="qa-date-range-picker"
|
28
|
+
horizontalAlign="right"
|
29
|
+
small
|
30
|
+
/>
|
31
|
+
</Fragment>
|
32
|
+
);
|
33
|
+
});
|
package/src/demo/index.ts
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
export * from './color-picker';
|
2
|
+
export * from './date-range-picker';
|
2
3
|
export * from './dropdown-state';
|
3
4
|
export * from './file-uploader';
|
4
5
|
export * from './number-input';
|
6
|
+
export * from './phone-number-input';
|
7
|
+
export * from './original-number-input';
|
8
|
+
export * from './input-date-mask';
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { FC, Fragment, useRef } from 'react';
|
2
|
+
|
3
|
+
import { observer } from 'mobx-react';
|
4
|
+
|
5
|
+
import { Text, Icon } from '@servicetitan/design-system';
|
6
|
+
|
7
|
+
import { FieldState } from 'formstate';
|
8
|
+
|
9
|
+
import { InputDateMask } from '..';
|
10
|
+
|
11
|
+
function useInputDateMaskField<T extends Date | undefined>(initial: T) {
|
12
|
+
return useRef(new FieldState(initial)).current;
|
13
|
+
}
|
14
|
+
|
15
|
+
export const InputDateMaskExample: FC = observer(() => {
|
16
|
+
const defaultField = useInputDateMaskField(undefined);
|
17
|
+
|
18
|
+
return (
|
19
|
+
<Fragment>
|
20
|
+
<Text size={4} className="m-b-half">
|
21
|
+
Default
|
22
|
+
</Text>
|
23
|
+
<InputDateMask
|
24
|
+
value={defaultField.value}
|
25
|
+
onChange={defaultField.onChange}
|
26
|
+
shortLabel={<Icon name="event" />}
|
27
|
+
/>
|
28
|
+
</Fragment>
|
29
|
+
);
|
30
|
+
});
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { FC, Fragment, useRef } from 'react';
|
2
|
+
|
3
|
+
import { observer } from 'mobx-react';
|
4
|
+
|
5
|
+
import { Text } from '@servicetitan/design-system';
|
6
|
+
|
7
|
+
import { FieldState } from 'formstate';
|
8
|
+
|
9
|
+
import { OrdinalNumberInput } from '..';
|
10
|
+
|
11
|
+
function useOrdinalNumberInputField<T extends number | undefined>(initial: T) {
|
12
|
+
return useRef(new FieldState(initial)).current;
|
13
|
+
}
|
14
|
+
|
15
|
+
export const OrdinalNumberInputExample: FC = observer(() => {
|
16
|
+
const defaultField = useOrdinalNumberInputField(undefined);
|
17
|
+
|
18
|
+
return (
|
19
|
+
<Fragment>
|
20
|
+
<Text size={4} className="m-b-half">
|
21
|
+
Default
|
22
|
+
</Text>
|
23
|
+
<OrdinalNumberInput
|
24
|
+
value={defaultField.value}
|
25
|
+
onChange={defaultField.onChange}
|
26
|
+
min={1}
|
27
|
+
max={31}
|
28
|
+
placeholder="1 - 31"
|
29
|
+
/>
|
30
|
+
</Fragment>
|
31
|
+
);
|
32
|
+
});
|
package/src/index.ts
CHANGED
@@ -8,3 +8,7 @@ export * from './dropdown-state';
|
|
8
8
|
export * from './form-helpers';
|
9
9
|
export * from './form-validators';
|
10
10
|
export * from './masked-input';
|
11
|
+
export * from './date-range-picker';
|
12
|
+
export * from './phone-number-input';
|
13
|
+
export * from './original-number-input';
|
14
|
+
export * from './input-date-mask';
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './input-date-mask';
|
@@ -0,0 +1,22 @@
|
|
1
|
+
.input {
|
2
|
+
font-size: var(--typescale-2);
|
3
|
+
flex-grow: 1;
|
4
|
+
}
|
5
|
+
|
6
|
+
.input :global(.label) {
|
7
|
+
background-color: var(--color-neutral-30);
|
8
|
+
border: 1px solid var(--color-neutral-60);
|
9
|
+
font-size: var(--typescale-3);
|
10
|
+
color: var(--color-neutral-90);
|
11
|
+
}
|
12
|
+
|
13
|
+
.input.error :global(.label) {
|
14
|
+
transition: all 0.2s ease-in-out;
|
15
|
+
color: var(--color-red-500) !important;
|
16
|
+
border-color: var(--color-red-500) !important;
|
17
|
+
background-color: var(--color-red-100) !important;
|
18
|
+
}
|
19
|
+
|
20
|
+
.input.error input:focus {
|
21
|
+
border-left-color: transparent !important;
|
22
|
+
}
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import { FC, ChangeEvent, useEffect, InputHTMLAttributes } from 'react';
|
2
|
+
import { Input, InputProps } from '@servicetitan/design-system';
|
3
|
+
import { action } from 'mobx';
|
4
|
+
import { useLocalStore, observer } from 'mobx-react';
|
5
|
+
import moment from 'moment';
|
6
|
+
import InputMask from 'react-input-mask';
|
7
|
+
import classnames from 'classnames';
|
8
|
+
import * as Styles from './input-date-mask.module.css';
|
9
|
+
|
10
|
+
const KEY_DEL = 46;
|
11
|
+
|
12
|
+
interface InputDateMaskStore {
|
13
|
+
maskValue: string;
|
14
|
+
setMask(maskValue: string): void;
|
15
|
+
}
|
16
|
+
|
17
|
+
export interface InputDateMaskProps extends Omit<InputProps, 'onChange' | 'value'> {
|
18
|
+
value?: Date;
|
19
|
+
['qa-testing']?: string;
|
20
|
+
maskChar?: string;
|
21
|
+
alwaysShowMask?: boolean;
|
22
|
+
dateFormat?: string;
|
23
|
+
minDate?: Date;
|
24
|
+
maxDate?: Date;
|
25
|
+
onChange(value: Date | undefined): void;
|
26
|
+
}
|
27
|
+
|
28
|
+
export const InputDateMask: FC<InputDateMaskProps> = observer(
|
29
|
+
({
|
30
|
+
className,
|
31
|
+
maskChar = 'X',
|
32
|
+
alwaysShowMask,
|
33
|
+
shortLabel,
|
34
|
+
value,
|
35
|
+
onChange,
|
36
|
+
'qa-testing': qaTestingLocator,
|
37
|
+
error,
|
38
|
+
disabled,
|
39
|
+
placeholder,
|
40
|
+
dateFormat = 'MM/DD/YYYY',
|
41
|
+
minDate,
|
42
|
+
maxDate,
|
43
|
+
}: InputDateMaskProps) => {
|
44
|
+
const state = useLocalStore(() => ({
|
45
|
+
maskValue: '',
|
46
|
+
setMask: action(function (this: InputDateMaskStore, mask: string) {
|
47
|
+
this.maskValue = mask;
|
48
|
+
}),
|
49
|
+
}));
|
50
|
+
|
51
|
+
const handleKeyDown = (evt: any) => {
|
52
|
+
if (evt.keyCode === KEY_DEL) {
|
53
|
+
onChange(undefined);
|
54
|
+
|
55
|
+
state.setMask('');
|
56
|
+
}
|
57
|
+
};
|
58
|
+
|
59
|
+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
60
|
+
const value = event.target.value;
|
61
|
+
|
62
|
+
state.setMask(value);
|
63
|
+
|
64
|
+
if (event.type === 'focus' || event.type === 'blur') {
|
65
|
+
return;
|
66
|
+
}
|
67
|
+
|
68
|
+
if (isMaskValid(value)) {
|
69
|
+
onChange(parseDate(value));
|
70
|
+
} else {
|
71
|
+
onChange(undefined);
|
72
|
+
}
|
73
|
+
};
|
74
|
+
|
75
|
+
useEffect(() => {
|
76
|
+
if (value === undefined && isMaskValid(state.maskValue)) {
|
77
|
+
state.setMask('');
|
78
|
+
}
|
79
|
+
}, [value]); // eslint-disable-line react-hooks/exhaustive-deps
|
80
|
+
|
81
|
+
const getMaskError = () => {
|
82
|
+
return value === undefined && state.maskValue !== '' && isMaskInvalid(state.maskValue);
|
83
|
+
};
|
84
|
+
|
85
|
+
const getValueError = () => {
|
86
|
+
return value !== undefined && !isDateInRange(value);
|
87
|
+
};
|
88
|
+
|
89
|
+
const formatDate = (date?: Date) => {
|
90
|
+
return date ? moment(date).format(dateFormat) : '';
|
91
|
+
};
|
92
|
+
|
93
|
+
const parseDate = (date: string) => {
|
94
|
+
return moment(date, dateFormat).toDate();
|
95
|
+
};
|
96
|
+
|
97
|
+
const isMaskValid = (mask: string) => {
|
98
|
+
return !mask.includes(maskChar) && isDateStringValid(mask);
|
99
|
+
};
|
100
|
+
|
101
|
+
const isMaskInvalid = (mask: string) => {
|
102
|
+
return !mask.includes(maskChar) && !isDateStringValid(mask);
|
103
|
+
};
|
104
|
+
|
105
|
+
const isDateStringValid = (date: string) => {
|
106
|
+
return moment(date, dateFormat).isValid() && isDateInRange(parseDate(date));
|
107
|
+
};
|
108
|
+
|
109
|
+
const isDateInRange = (date: Date) => {
|
110
|
+
if (minDate && maxDate) {
|
111
|
+
return date >= minDate && date <= maxDate;
|
112
|
+
} else if (minDate) {
|
113
|
+
return date >= minDate;
|
114
|
+
} else if (maxDate) {
|
115
|
+
return date <= maxDate;
|
116
|
+
}
|
117
|
+
|
118
|
+
return true;
|
119
|
+
};
|
120
|
+
|
121
|
+
const mask = dateFormat.replace(/[MDY]/g, '9');
|
122
|
+
|
123
|
+
const realError = error || getMaskError() || getValueError();
|
124
|
+
|
125
|
+
const realPlaceholder = placeholder || mask.replace(/9/g, maskChar);
|
126
|
+
|
127
|
+
const classes = classnames(className, qaTestingLocator, Styles.input, {
|
128
|
+
[Styles.error]: realError,
|
129
|
+
});
|
130
|
+
|
131
|
+
return (
|
132
|
+
<InputMask
|
133
|
+
mask={mask}
|
134
|
+
maskChar={maskChar}
|
135
|
+
onChange={handleChange}
|
136
|
+
onKeyDown={handleKeyDown}
|
137
|
+
alwaysShowMask={alwaysShowMask}
|
138
|
+
value={formatDate(value) || state.maskValue}
|
139
|
+
>
|
140
|
+
{({ size, ...props }: InputHTMLAttributes<HTMLInputElement>) => (
|
141
|
+
<div>
|
142
|
+
<Input
|
143
|
+
{...props}
|
144
|
+
shortLabel={shortLabel}
|
145
|
+
className={classes}
|
146
|
+
small
|
147
|
+
fluid
|
148
|
+
error={realError}
|
149
|
+
disabled={disabled}
|
150
|
+
placeholder={realPlaceholder}
|
151
|
+
/>
|
152
|
+
</div>
|
153
|
+
)}
|
154
|
+
</InputMask>
|
155
|
+
);
|
156
|
+
}
|
157
|
+
);
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import { parseIntIntoRange, ordinalString } from '../ordinal-number-input';
|
2
|
+
|
3
|
+
describe('[Common] OrdinalNumberInput', () => {
|
4
|
+
describe('parseDayInput', () => {
|
5
|
+
it('parses numeric input.', () => {
|
6
|
+
expect(parseIntIntoRange('2', 1, 31)).toEqual(2);
|
7
|
+
});
|
8
|
+
it('parses mixed input starting with number.', () => {
|
9
|
+
expect(parseIntIntoRange('2asdf', 1, 31)).toEqual(2);
|
10
|
+
});
|
11
|
+
it('sets input value to empty string when cannot parse.', () => {
|
12
|
+
expect(parseIntIntoRange('asdf2', 1, 31)).toEqual(undefined);
|
13
|
+
});
|
14
|
+
it('forces input lower than bounds into valid range.', () => {
|
15
|
+
expect(parseIntIntoRange('0', 1, 31)).toEqual(1);
|
16
|
+
});
|
17
|
+
it('forces input higher than bounds into valid range.', () => {
|
18
|
+
expect(parseIntIntoRange('32', 1, 31)).toEqual(31);
|
19
|
+
});
|
20
|
+
});
|
21
|
+
describe('formatDay', () => {
|
22
|
+
it("appends 'st'", () => {
|
23
|
+
expect(ordinalString(1)).toEqual('1st');
|
24
|
+
expect(ordinalString(21)).toEqual('21st');
|
25
|
+
});
|
26
|
+
it("appends 'nd'", () => {
|
27
|
+
expect(ordinalString(2)).toEqual('2nd');
|
28
|
+
expect(ordinalString(22)).toEqual('22nd');
|
29
|
+
});
|
30
|
+
it("appends 'rd'", () => {
|
31
|
+
expect(ordinalString(3)).toEqual('3rd');
|
32
|
+
expect(ordinalString(23)).toEqual('23rd');
|
33
|
+
});
|
34
|
+
it("appends 'th'", () => {
|
35
|
+
expect(ordinalString(4)).toEqual('4th');
|
36
|
+
expect(ordinalString(24)).toEqual('24th');
|
37
|
+
});
|
38
|
+
it("appends 'th' to 11", () => {
|
39
|
+
expect(ordinalString(11)).toEqual('11th');
|
40
|
+
expect(ordinalString(111)).toEqual('111th');
|
41
|
+
});
|
42
|
+
it("appends 'th' to 12", () => {
|
43
|
+
expect(ordinalString(12)).toEqual('12th');
|
44
|
+
expect(ordinalString(112)).toEqual('112th');
|
45
|
+
});
|
46
|
+
it("appends 'th' to 13", () => {
|
47
|
+
expect(ordinalString(13)).toEqual('13th');
|
48
|
+
expect(ordinalString(113)).toEqual('113th');
|
49
|
+
});
|
50
|
+
});
|
51
|
+
});
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './ordinal-number-input';
|