@servicetitan/form 14.3.0 → 16.0.2

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 (78) hide show
  1. package/dist/date-range-picker/date-range-picker.d.ts +47 -0
  2. package/dist/date-range-picker/date-range-picker.d.ts.map +1 -0
  3. package/dist/date-range-picker/date-range-picker.js +256 -0
  4. package/dist/date-range-picker/date-range-picker.js.map +1 -0
  5. package/dist/date-range-picker/date-range-picker.module.css +37 -0
  6. package/dist/date-range-picker/index.d.ts +2 -0
  7. package/dist/date-range-picker/index.d.ts.map +1 -0
  8. package/dist/date-range-picker/index.js +14 -0
  9. package/dist/date-range-picker/index.js.map +1 -0
  10. package/dist/demo/date-range-picker.d.ts +3 -0
  11. package/dist/demo/date-range-picker.d.ts.map +1 -0
  12. package/dist/demo/date-range-picker.js +17 -0
  13. package/dist/demo/date-range-picker.js.map +1 -0
  14. package/dist/demo/index.d.ts +4 -0
  15. package/dist/demo/index.d.ts.map +1 -1
  16. package/dist/demo/index.js +4 -0
  17. package/dist/demo/index.js.map +1 -1
  18. package/dist/demo/input-date-mask.d.ts +3 -0
  19. package/dist/demo/input-date-mask.d.ts.map +1 -0
  20. package/dist/demo/input-date-mask.js +17 -0
  21. package/dist/demo/input-date-mask.js.map +1 -0
  22. package/dist/demo/original-number-input.d.ts +3 -0
  23. package/dist/demo/original-number-input.d.ts.map +1 -0
  24. package/dist/demo/original-number-input.js +17 -0
  25. package/dist/demo/original-number-input.js.map +1 -0
  26. package/dist/demo/phone-number-input.d.ts +3 -0
  27. package/dist/demo/phone-number-input.d.ts.map +1 -0
  28. package/dist/demo/phone-number-input.js +8 -0
  29. package/dist/demo/phone-number-input.js.map +1 -0
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/input-date-mask/index.d.ts +2 -0
  35. package/dist/input-date-mask/index.d.ts.map +1 -0
  36. package/dist/input-date-mask/index.js +14 -0
  37. package/dist/input-date-mask/index.js.map +1 -0
  38. package/dist/input-date-mask/input-date-mask.d.ts +14 -0
  39. package/dist/input-date-mask/input-date-mask.d.ts.map +1 -0
  40. package/dist/input-date-mask/input-date-mask.js +122 -0
  41. package/dist/input-date-mask/input-date-mask.js.map +1 -0
  42. package/dist/input-date-mask/input-date-mask.module.css +22 -0
  43. package/dist/original-number-input/index.d.ts +2 -0
  44. package/dist/original-number-input/index.d.ts.map +1 -0
  45. package/dist/original-number-input/index.js +14 -0
  46. package/dist/original-number-input/index.js.map +1 -0
  47. package/dist/original-number-input/ordinal-number-input.d.ts +22 -0
  48. package/dist/original-number-input/ordinal-number-input.d.ts.map +1 -0
  49. package/dist/original-number-input/ordinal-number-input.js +149 -0
  50. package/dist/original-number-input/ordinal-number-input.js.map +1 -0
  51. package/dist/phone-number-input/index.d.ts +2 -0
  52. package/dist/phone-number-input/index.d.ts.map +1 -0
  53. package/dist/phone-number-input/index.js +14 -0
  54. package/dist/phone-number-input/index.js.map +1 -0
  55. package/dist/phone-number-input/phone-number-input.d.ts +7 -0
  56. package/dist/phone-number-input/phone-number-input.d.ts.map +1 -0
  57. package/dist/phone-number-input/phone-number-input.js +26 -0
  58. package/dist/phone-number-input/phone-number-input.js.map +1 -0
  59. package/package.json +20 -13
  60. package/src/date-range-picker/date-range-picker.module.css +37 -0
  61. package/src/date-range-picker/date-range-picker.module.css.d.ts +7 -0
  62. package/src/date-range-picker/date-range-picker.tsx +297 -0
  63. package/src/date-range-picker/index.ts +1 -0
  64. package/src/demo/date-range-picker.tsx +33 -0
  65. package/src/demo/index.ts +4 -0
  66. package/src/demo/input-date-mask.tsx +30 -0
  67. package/src/demo/original-number-input.tsx +32 -0
  68. package/src/demo/phone-number-input.tsx +9 -0
  69. package/src/index.ts +4 -0
  70. package/src/input-date-mask/index.ts +1 -0
  71. package/src/input-date-mask/input-date-mask.module.css +22 -0
  72. package/src/input-date-mask/input-date-mask.module.css.d.ts +4 -0
  73. package/src/input-date-mask/input-date-mask.tsx +157 -0
  74. package/src/original-number-input/__tests__/ordinal-number-input.test.tsx +51 -0
  75. package/src/original-number-input/index.ts +1 -0
  76. package/src/original-number-input/ordinal-number-input.tsx +111 -0
  77. package/src/phone-number-input/index.ts +1 -0
  78. 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
+ });
@@ -0,0 +1,9 @@
1
+ import { FC } from 'react';
2
+
3
+ import { observer } from 'mobx-react';
4
+
5
+ import { PhoneNumberInput } from '..';
6
+
7
+ export const PhoneNumberInputExample: FC = observer(() => (
8
+ <PhoneNumberInput placeholder="(123) 456-7890" label="Phone" />
9
+ ));
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,4 @@
1
+ export const __esModule: true;
2
+ export const input: string;
3
+ export const error: string;
4
+
@@ -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';