@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.
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';