-
Notifications
You must be signed in to change notification settings - Fork 71
[LG-5504] feat(input-box): add input-box package with utility functions for date/time input handling #3265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[LG-5504] feat(input-box): add input-box package with utility functions for date/time input handling #3265
Changes from 10 commits
40a106d
ceb124e
bde44d5
ae8aec0
628097c
6745147
f976e42
fbf6131
c61bbb2
3e515d7
aff81dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Internal Input Box | ||
|
|
||
| An internal component intended to be used by any date or time component. | ||
| I.e. `DatePicker`, `TimeInput` etc. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
|
|
||
| { | ||
| "name": "@leafygreen-ui/input-box", | ||
| "version": "0.0.1", | ||
| "description": "LeafyGreen UI Kit Input Box", | ||
| "main": "./dist/umd/index.js", | ||
| "module": "./dist/esm/index.js", | ||
| "types": "./dist/types/index.d.ts", | ||
| "license": "Apache-2.0", | ||
| "exports": { | ||
| ".": { | ||
| "require": "./dist/umd/index.js", | ||
| "import": "./dist/esm/index.js", | ||
| "types": "./dist/types/index.d.ts" | ||
| }, | ||
| "./testing": { | ||
| "require": "./dist/umd/testing/index.js", | ||
| "import": "./dist/esm/testing/index.js", | ||
| "types": "./dist/types/testing/index.d.ts" | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "build": "lg-build bundle", | ||
| "tsc": "lg-build tsc", | ||
| "docs": "lg-build docs" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "dependencies": { | ||
| "@leafygreen-ui/emotion": "workspace:^", | ||
| "@leafygreen-ui/lib": "workspace:^", | ||
| "@leafygreen-ui/hooks": "workspace:^", | ||
| "@leafygreen-ui/date-utils": "workspace:^", | ||
| "@leafygreen-ui/palette": "workspace:^", | ||
| "@leafygreen-ui/tokens": "workspace:^", | ||
| "@leafygreen-ui/typography": "workspace:^" | ||
| }, | ||
| "peerDependencies": { | ||
| "@leafygreen-ui/leafygreen-provider": "workspace:^" | ||
| }, | ||
| "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/input-box", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/mongodb/leafygreen-ui" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://jira.mongodb.org/projects/LG/summary" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| export { | ||
| createExplicitSegmentValidator, | ||
| type ExplicitSegmentRule, | ||
| isElementInputSegment, | ||
| isValidValueForSegment, | ||
| } from './utils'; | ||
| export { getValueFormatter } from './utils/getValueFormatter/getValueFormatter'; | ||
| export { | ||
| isValidSegmentName, | ||
| isValidSegmentValue, | ||
| } from './utils/isValidSegment/isValidSegment'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import { createExplicitSegmentValidator } from './createExplicitSegmentValidator'; | ||
|
|
||
| const segmentObj = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we rename this to something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I updated this example to include a few more items in the object, just to show a few more segments, so |
||
| Day: 'day', | ||
| Month: 'month', | ||
| Year: 'year', | ||
| Hour: 'hour', | ||
| Minute: 'minute', | ||
| } as const; | ||
|
|
||
| const rules = { | ||
| day: { maxChars: 2, minExplicitValue: 4 }, | ||
| month: { maxChars: 2, minExplicitValue: 2 }, | ||
| year: { maxChars: 4 }, // any 4-digit year | ||
| hour: { maxChars: 2, minExplicitValue: 3 }, | ||
| minute: { maxChars: 2, minExplicitValue: 6 }, | ||
| }; | ||
|
|
||
| const isExplicitSegmentValue = createExplicitSegmentValidator({ | ||
| segmentEnum: segmentObj, | ||
| rules, | ||
| }); | ||
|
|
||
| describe('packages/input-box/utils/createExplicitSegmentValidator', () => { | ||
| describe('day segment', () => { | ||
| test('returns false for single digit below minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('day', '1')).toBe(false); | ||
| expect(isExplicitSegmentValue('day', '2')).toBe(false); | ||
| expect(isExplicitSegmentValue('day', '3')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns true for single digit at or above minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('day', '4')).toBe(true); | ||
| expect(isExplicitSegmentValue('day', '5')).toBe(true); | ||
| expect(isExplicitSegmentValue('day', '9')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns true for two-digit values (maxChars)', () => { | ||
| expect(isExplicitSegmentValue('day', '01')).toBe(true); | ||
| expect(isExplicitSegmentValue('day', '10')).toBe(true); | ||
| expect(isExplicitSegmentValue('day', '22')).toBe(true); | ||
| expect(isExplicitSegmentValue('day', '31')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns false for invalid values', () => { | ||
| expect(isExplicitSegmentValue('day', '0')).toBe(false); | ||
| expect(isExplicitSegmentValue('day', '')).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('month segment', () => { | ||
| test('returns false for single digit below minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('month', '1')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns true for single digit at or above minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('month', '2')).toBe(true); | ||
| expect(isExplicitSegmentValue('month', '3')).toBe(true); | ||
| expect(isExplicitSegmentValue('month', '9')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns true for two-digit values (maxChars)', () => { | ||
| expect(isExplicitSegmentValue('month', '01')).toBe(true); | ||
| expect(isExplicitSegmentValue('month', '12')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns false for invalid values', () => { | ||
| expect(isExplicitSegmentValue('month', '0')).toBe(false); | ||
| expect(isExplicitSegmentValue('month', '')).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('year segment', () => { | ||
| test('returns false for values shorter than maxChars', () => { | ||
| expect(isExplicitSegmentValue('year', '1')).toBe(false); | ||
| expect(isExplicitSegmentValue('year', '20')).toBe(false); | ||
| expect(isExplicitSegmentValue('year', '200')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns true for four-digit values (maxChars)', () => { | ||
| expect(isExplicitSegmentValue('year', '1970')).toBe(true); | ||
| expect(isExplicitSegmentValue('year', '2000')).toBe(true); | ||
| expect(isExplicitSegmentValue('year', '2023')).toBe(true); | ||
| expect(isExplicitSegmentValue('year', '0001')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns false for invalid values', () => { | ||
| expect(isExplicitSegmentValue('year', '0')).toBe(false); | ||
| expect(isExplicitSegmentValue('year', '')).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('hour segment', () => { | ||
| test('returns false for single digit below minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('hour', '1')).toBe(false); | ||
| expect(isExplicitSegmentValue('hour', '0')).toBe(false); | ||
| expect(isExplicitSegmentValue('hour', '2')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns true for single digit at or above minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('hour', '3')).toBe(true); | ||
| expect(isExplicitSegmentValue('hour', '9')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns true for two-digit values at or above minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('hour', '12')).toBe(true); | ||
| expect(isExplicitSegmentValue('hour', '23')).toBe(true); | ||
| expect(isExplicitSegmentValue('hour', '05')).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('minute segment', () => { | ||
| test('returns false for single digit below minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('minute', '0')).toBe(false); | ||
| expect(isExplicitSegmentValue('minute', '1')).toBe(false); | ||
| expect(isExplicitSegmentValue('minute', '5')).toBe(false); | ||
| }); | ||
|
|
||
| test('returns true for single digit at or above minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('minute', '6')).toBe(true); | ||
| expect(isExplicitSegmentValue('minute', '7')).toBe(true); | ||
| expect(isExplicitSegmentValue('minute', '9')).toBe(true); | ||
| }); | ||
|
|
||
| test('returns true for two-digit values at or above minExplicitValue', () => { | ||
| expect(isExplicitSegmentValue('minute', '59')).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('invalid segment names', () => { | ||
| test('returns false for unknown segment names', () => { | ||
| // @ts-expect-error Testing invalid segment | ||
| expect(isExplicitSegmentValue('invalid', '10')).toBe(false); | ||
| // @ts-expect-error Testing invalid segment | ||
| expect(isExplicitSegmentValue('millisecond', '12')).toBe(false); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { | ||
| isValidSegmentName, | ||
| isValidSegmentValue, | ||
| } from '../isValidSegment/isValidSegment'; | ||
|
|
||
| /** | ||
| * Configuration for determining if a segment value is an explicit, unique value for a given segment. | ||
| */ | ||
| export interface ExplicitSegmentRule { | ||
| /** Maximum characters for this segment */ | ||
| maxChars: number; | ||
| /** Minimum numeric value that makes the input explicit (optional) */ | ||
| minExplicitValue?: number; | ||
| } | ||
|
|
||
| /** | ||
| * Factory function that creates a segment value validator that checks if a segment value is an explicit, unique value for a given segment. | ||
| * | ||
| * An "explicit" segment value is one that is complete and unambiguous, eliminating the possibility that it is a partial input. | ||
| * A value is considered explicit if it meets one of two conditions: | ||
| * 1. **Maximum Length:** The value has been padded (e.g., with leading zeros) to reach the segment's maximum character length (`maxChars`). | ||
| * *(Example: For `maxChars: 2`, '01' is explicit, but '1' is not).* | ||
| * 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. | ||
| * *(Example: For `minExplicitValue: 4`, '4' is explicit, but '1' is potentially ambiguous).* | ||
| * | ||
| * @param segmentEnum - The segment enum/object containing the segment names and their corresponding values to validate against | ||
| * @param rules - Rules for each segment type | ||
| * @returns A function that checks if a segment value is explicit | ||
| * | ||
| * @example | ||
| * const segmentObj = { | ||
| * Day: 'day', | ||
| * Month: 'month', | ||
| * Year: 'year', | ||
| * Hour: 'hour', | ||
| * Minute: 'minute', | ||
| * }; | ||
| * const rules = { | ||
| * day: { maxChars: 2, minExplicitValue: 4 }, | ||
| * month: { maxChars: 2, minExplicitValue: 2 }, | ||
| * year: { maxChars: 4 }, | ||
| * hour: { maxChars: 2, minExplicitValue: 3 }, | ||
| * minute: { maxChars: 2, minExplicitValue: 6 }, | ||
| * }; | ||
| * | ||
| * // Contrast this with an ambiguous segment value: | ||
| * // Explicit: Day = '4' (meets min value), '02' (meets max length) | ||
| * // Ambiguous: Day = '2' (does not meet max length and is less than min value) | ||
| * | ||
| * const isExplicitSegmentValue = createExplicitSegmentValidator({ | ||
| * segmentEnum, | ||
| * rules, | ||
| * }); | ||
| * | ||
| * isExplicitSegmentValue('day', '1'); // false (Ambiguous - below min value and max length) | ||
| * isExplicitSegmentValue('day', '01'); // true (Explicit - meets max length) | ||
| * isExplicitSegmentValue('day', '4'); // true (Explicit - meets min value) | ||
| * isExplicitSegmentValue('year', '2000'); // true (Explicit - meets max length) | ||
| * isExplicitSegmentValue('year', '1'); // false (Ambiguous - below max length) | ||
| * isExplicitSegmentValue('hour', '05'); // true (Explicit - meets min value) | ||
| * isExplicitSegmentValue('hour', '23'); // true (Explicit - meets max length) | ||
| * isExplicitSegmentValue('hour', '2'); // false (Ambiguous - below min value) | ||
| * isExplicitSegmentValue('minute', '07'); // true (Explicit - meets min value) | ||
| * isExplicitSegmentValue('minute', '59'); // true (Explicit - meets max length) | ||
| * isExplicitSegmentValue('minute', '5'); // false (Ambiguous - below min value) | ||
| */ | ||
| export function createExplicitSegmentValidator< | ||
| SegmentEnum extends Record<string, string>, | ||
| >({ | ||
| segmentEnum, | ||
| rules, | ||
| }: { | ||
| segmentEnum: SegmentEnum; | ||
| rules: Record<SegmentEnum[keyof SegmentEnum], ExplicitSegmentRule>; | ||
| }) { | ||
| return (segment: SegmentEnum[keyof SegmentEnum], value: string): boolean => { | ||
| if ( | ||
| !(isValidSegmentValue(value) && isValidSegmentName(segmentEnum, segment)) | ||
| ) | ||
| return false; | ||
|
|
||
| const rule = rules[segment]; | ||
| if (!rule) return false; | ||
|
|
||
| const isMaxLength = value.length === rule.maxChars; | ||
| const meetsMinValue = rule.minExplicitValue | ||
| ? Number(value) >= rule.minExplicitValue | ||
| : false; | ||
|
|
||
| return isMaxLength || meetsMinValue; | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should these go in the date-utils package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe so. date-utils are for Date manipulation and comparison utils not included in the
date-fnslibrary. These are more implementation-specific utils that used to live indate-picker/src/shared/utils.