Skip to content

Commit 5c277ac

Browse files
authored
[LG-5504] feat(input-box): add input-box package with utility functions for date/time input handling (#3265)
* feat(input-box): adds input-box package and utils * refactor(input-box): update segment validator and input value utilities for improved type safety and consistency * fix(input-box): update explicit segment validation rules for day, month, and year * docs(input-box): enhance JSDoc comments for explicit segment validator with detailed explanations and examples * refactor(input-box): rename shouldNotRollover to shouldRollover for clarity in segment value handling * feat(input-box): add hour and minute segments to explicit segment validator with corresponding validation rules and tests * refactor(input-box): rename shouldRollover to shouldWrap for clarity in segment value handling * docs(input-box): improve JSDoc comment for segmentEnum parameter in createExplicitSegmentValidator * feat(input-box): enhance segment value handling by adding minute segment and updating validation rules for day and year segments * feat(input-box): update validation rules for year segment to allow values from 1970 to 2038 and add custom validation for specific range * docs(input-box): clarify JSDoc comments for getValueFormatter parameters
1 parent 0532a24 commit 5c277ac

22 files changed

+2120
-0
lines changed

packages/input-box/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Internal Input Box
2+
3+
An internal component intended to be used by any date or time component.
4+
I.e. `DatePicker`, `TimeInput` etc.

packages/input-box/package.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
{
3+
"name": "@leafygreen-ui/input-box",
4+
"version": "0.0.1",
5+
"description": "LeafyGreen UI Kit Input Box",
6+
"main": "./dist/umd/index.js",
7+
"module": "./dist/esm/index.js",
8+
"types": "./dist/types/index.d.ts",
9+
"license": "Apache-2.0",
10+
"exports": {
11+
".": {
12+
"require": "./dist/umd/index.js",
13+
"import": "./dist/esm/index.js",
14+
"types": "./dist/types/index.d.ts"
15+
},
16+
"./testing": {
17+
"require": "./dist/umd/testing/index.js",
18+
"import": "./dist/esm/testing/index.js",
19+
"types": "./dist/types/testing/index.d.ts"
20+
}
21+
},
22+
"scripts": {
23+
"build": "lg-build bundle",
24+
"tsc": "lg-build tsc",
25+
"docs": "lg-build docs"
26+
},
27+
"publishConfig": {
28+
"access": "public"
29+
},
30+
"dependencies": {
31+
"@leafygreen-ui/emotion": "workspace:^",
32+
"@leafygreen-ui/lib": "workspace:^",
33+
"@leafygreen-ui/hooks": "workspace:^",
34+
"@leafygreen-ui/date-utils": "workspace:^",
35+
"@leafygreen-ui/palette": "workspace:^",
36+
"@leafygreen-ui/tokens": "workspace:^",
37+
"@leafygreen-ui/typography": "workspace:^"
38+
},
39+
"peerDependencies": {
40+
"@leafygreen-ui/leafygreen-provider": "workspace:^"
41+
},
42+
"homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/input-box",
43+
"repository": {
44+
"type": "git",
45+
"url": "https://github.com/mongodb/leafygreen-ui"
46+
},
47+
"bugs": {
48+
"url": "https://jira.mongodb.org/projects/LG/summary"
49+
}
50+
}

packages/input-box/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export {
2+
createExplicitSegmentValidator,
3+
type ExplicitSegmentRule,
4+
isElementInputSegment,
5+
isValidValueForSegment,
6+
} from './utils';
7+
export { getValueFormatter } from './utils/getValueFormatter/getValueFormatter';
8+
export {
9+
isValidSegmentName,
10+
isValidSegmentValue,
11+
} from './utils/isValidSegment/isValidSegment';
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { createExplicitSegmentValidator } from './createExplicitSegmentValidator';
2+
3+
const segmentObj = {
4+
Day: 'day',
5+
Month: 'month',
6+
Year: 'year',
7+
Hour: 'hour',
8+
Minute: 'minute',
9+
} as const;
10+
11+
const rules = {
12+
day: { maxChars: 2, minExplicitValue: 4 },
13+
month: { maxChars: 2, minExplicitValue: 2 },
14+
year: { maxChars: 4 }, // any 4-digit year
15+
hour: { maxChars: 2, minExplicitValue: 3 },
16+
minute: { maxChars: 2, minExplicitValue: 6 },
17+
};
18+
19+
const isExplicitSegmentValue = createExplicitSegmentValidator({
20+
segmentEnum: segmentObj,
21+
rules,
22+
});
23+
24+
describe('packages/input-box/utils/createExplicitSegmentValidator', () => {
25+
describe('day segment', () => {
26+
test('returns false for single digit below minExplicitValue', () => {
27+
expect(isExplicitSegmentValue('day', '1')).toBe(false);
28+
expect(isExplicitSegmentValue('day', '2')).toBe(false);
29+
expect(isExplicitSegmentValue('day', '3')).toBe(false);
30+
});
31+
32+
test('returns true for single digit at or above minExplicitValue', () => {
33+
expect(isExplicitSegmentValue('day', '4')).toBe(true);
34+
expect(isExplicitSegmentValue('day', '5')).toBe(true);
35+
expect(isExplicitSegmentValue('day', '9')).toBe(true);
36+
});
37+
38+
test('returns true for two-digit values (maxChars)', () => {
39+
expect(isExplicitSegmentValue('day', '01')).toBe(true);
40+
expect(isExplicitSegmentValue('day', '10')).toBe(true);
41+
expect(isExplicitSegmentValue('day', '22')).toBe(true);
42+
expect(isExplicitSegmentValue('day', '31')).toBe(true);
43+
});
44+
45+
test('returns false for invalid values', () => {
46+
expect(isExplicitSegmentValue('day', '0')).toBe(false);
47+
expect(isExplicitSegmentValue('day', '')).toBe(false);
48+
});
49+
});
50+
51+
describe('month segment', () => {
52+
test('returns false for single digit below minExplicitValue', () => {
53+
expect(isExplicitSegmentValue('month', '1')).toBe(false);
54+
});
55+
56+
test('returns true for single digit at or above minExplicitValue', () => {
57+
expect(isExplicitSegmentValue('month', '2')).toBe(true);
58+
expect(isExplicitSegmentValue('month', '3')).toBe(true);
59+
expect(isExplicitSegmentValue('month', '9')).toBe(true);
60+
});
61+
62+
test('returns true for two-digit values (maxChars)', () => {
63+
expect(isExplicitSegmentValue('month', '01')).toBe(true);
64+
expect(isExplicitSegmentValue('month', '12')).toBe(true);
65+
});
66+
67+
test('returns false for invalid values', () => {
68+
expect(isExplicitSegmentValue('month', '0')).toBe(false);
69+
expect(isExplicitSegmentValue('month', '')).toBe(false);
70+
});
71+
});
72+
73+
describe('year segment', () => {
74+
test('returns false for values shorter than maxChars', () => {
75+
expect(isExplicitSegmentValue('year', '1')).toBe(false);
76+
expect(isExplicitSegmentValue('year', '20')).toBe(false);
77+
expect(isExplicitSegmentValue('year', '200')).toBe(false);
78+
});
79+
80+
test('returns true for four-digit values (maxChars)', () => {
81+
expect(isExplicitSegmentValue('year', '1970')).toBe(true);
82+
expect(isExplicitSegmentValue('year', '2000')).toBe(true);
83+
expect(isExplicitSegmentValue('year', '2023')).toBe(true);
84+
expect(isExplicitSegmentValue('year', '0001')).toBe(true);
85+
});
86+
87+
test('returns false for invalid values', () => {
88+
expect(isExplicitSegmentValue('year', '0')).toBe(false);
89+
expect(isExplicitSegmentValue('year', '')).toBe(false);
90+
});
91+
});
92+
93+
describe('hour segment', () => {
94+
test('returns false for single digit below minExplicitValue', () => {
95+
expect(isExplicitSegmentValue('hour', '1')).toBe(false);
96+
expect(isExplicitSegmentValue('hour', '0')).toBe(false);
97+
expect(isExplicitSegmentValue('hour', '2')).toBe(false);
98+
});
99+
100+
test('returns true for single digit at or above minExplicitValue', () => {
101+
expect(isExplicitSegmentValue('hour', '3')).toBe(true);
102+
expect(isExplicitSegmentValue('hour', '9')).toBe(true);
103+
});
104+
105+
test('returns true for two-digit values at or above minExplicitValue', () => {
106+
expect(isExplicitSegmentValue('hour', '12')).toBe(true);
107+
expect(isExplicitSegmentValue('hour', '23')).toBe(true);
108+
expect(isExplicitSegmentValue('hour', '05')).toBe(true);
109+
});
110+
});
111+
112+
describe('minute segment', () => {
113+
test('returns false for single digit below minExplicitValue', () => {
114+
expect(isExplicitSegmentValue('minute', '0')).toBe(false);
115+
expect(isExplicitSegmentValue('minute', '1')).toBe(false);
116+
expect(isExplicitSegmentValue('minute', '5')).toBe(false);
117+
});
118+
119+
test('returns true for single digit at or above minExplicitValue', () => {
120+
expect(isExplicitSegmentValue('minute', '6')).toBe(true);
121+
expect(isExplicitSegmentValue('minute', '7')).toBe(true);
122+
expect(isExplicitSegmentValue('minute', '9')).toBe(true);
123+
});
124+
125+
test('returns true for two-digit values at or above minExplicitValue', () => {
126+
expect(isExplicitSegmentValue('minute', '59')).toBe(true);
127+
});
128+
});
129+
130+
describe('invalid segment names', () => {
131+
test('returns false for unknown segment names', () => {
132+
// @ts-expect-error Testing invalid segment
133+
expect(isExplicitSegmentValue('invalid', '10')).toBe(false);
134+
// @ts-expect-error Testing invalid segment
135+
expect(isExplicitSegmentValue('millisecond', '12')).toBe(false);
136+
});
137+
});
138+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
isValidSegmentName,
3+
isValidSegmentValue,
4+
} from '../isValidSegment/isValidSegment';
5+
6+
/**
7+
* Configuration for determining if a segment value is an explicit, unique value for a given segment.
8+
*/
9+
export interface ExplicitSegmentRule {
10+
/** Maximum characters for this segment */
11+
maxChars: number;
12+
/** Minimum numeric value that makes the input explicit (optional) */
13+
minExplicitValue?: number;
14+
}
15+
16+
/**
17+
* Factory function that creates a segment value validator that checks if a segment value is an explicit, unique value for a given segment.
18+
*
19+
* An "explicit" segment value is one that is complete and unambiguous, eliminating the possibility that it is a partial input.
20+
* A value is considered explicit if it meets one of two conditions:
21+
* 1. **Maximum Length:** The value has been padded (e.g., with leading zeros) to reach the segment's maximum character length (`maxChars`).
22+
* *(Example: For `maxChars: 2`, '01' is explicit, but '1' is not).*
23+
* 2. **Minimum Value Threshold:** The value, while shorter than `maxChars`, is numerically equal to or greater than the segment's defined `minExplicitValue`. This ensures single-digit inputs are treated as final values rather than the start of a multi-digit entry.
24+
* *(Example: For `minExplicitValue: 4`, '4' is explicit, but '1' is potentially ambiguous).*
25+
*
26+
* @param segmentEnum - The segment enum/object containing the segment names and their corresponding values to validate against
27+
* @param rules - Rules for each segment type
28+
* @returns A function that checks if a segment value is explicit
29+
*
30+
* @example
31+
* const segmentObj = {
32+
* Day: 'day',
33+
* Month: 'month',
34+
* Year: 'year',
35+
* Hour: 'hour',
36+
* Minute: 'minute',
37+
* };
38+
* const rules = {
39+
* day: { maxChars: 2, minExplicitValue: 4 },
40+
* month: { maxChars: 2, minExplicitValue: 2 },
41+
* year: { maxChars: 4 },
42+
* hour: { maxChars: 2, minExplicitValue: 3 },
43+
* minute: { maxChars: 2, minExplicitValue: 6 },
44+
* };
45+
*
46+
* // Contrast this with an ambiguous segment value:
47+
* // Explicit: Day = '4' (meets min value), '02' (meets max length)
48+
* // Ambiguous: Day = '2' (does not meet max length and is less than min value)
49+
*
50+
* const isExplicitSegmentValue = createExplicitSegmentValidator({
51+
* segmentEnum,
52+
* rules,
53+
* });
54+
*
55+
* isExplicitSegmentValue('day', '1'); // false (Ambiguous - below min value and max length)
56+
* isExplicitSegmentValue('day', '01'); // true (Explicit - meets max length)
57+
* isExplicitSegmentValue('day', '4'); // true (Explicit - meets min value)
58+
* isExplicitSegmentValue('year', '2000'); // true (Explicit - meets max length)
59+
* isExplicitSegmentValue('year', '1'); // false (Ambiguous - below max length)
60+
* isExplicitSegmentValue('hour', '05'); // true (Explicit - meets min value)
61+
* isExplicitSegmentValue('hour', '23'); // true (Explicit - meets max length)
62+
* isExplicitSegmentValue('hour', '2'); // false (Ambiguous - below min value)
63+
* isExplicitSegmentValue('minute', '07'); // true (Explicit - meets min value)
64+
* isExplicitSegmentValue('minute', '59'); // true (Explicit - meets max length)
65+
* isExplicitSegmentValue('minute', '5'); // false (Ambiguous - below min value)
66+
*/
67+
export function createExplicitSegmentValidator<
68+
SegmentEnum extends Record<string, string>,
69+
>({
70+
segmentEnum,
71+
rules,
72+
}: {
73+
segmentEnum: SegmentEnum;
74+
rules: Record<SegmentEnum[keyof SegmentEnum], ExplicitSegmentRule>;
75+
}) {
76+
return (segment: SegmentEnum[keyof SegmentEnum], value: string): boolean => {
77+
if (
78+
!(isValidSegmentValue(value) && isValidSegmentName(segmentEnum, segment))
79+
)
80+
return false;
81+
82+
const rule = rules[segment];
83+
if (!rule) return false;
84+
85+
const isMaxLength = value.length === rule.maxChars;
86+
const meetsMinValue = rule.minExplicitValue
87+
? Number(value) >= rule.minExplicitValue
88+
: false;
89+
90+
return isMaxLength || meetsMinValue;
91+
};
92+
}

0 commit comments

Comments
 (0)