Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/input-box/README.md
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.
50 changes: 50 additions & 0 deletions packages/input-box/package.json
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"
}
}
11 changes: 11 additions & 0 deletions packages/input-box/src/index.ts
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';
Comment on lines +1 to +11
Copy link
Collaborator

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?

Copy link
Collaborator Author

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-fns library. These are more implementation-specific utils that used to live in date-picker/src/shared/utils.

Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { createExplicitSegmentValidator } from './createExplicitSegmentValidator';

const segmentObj = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we rename this to something like dateSegmentObj? Might be a result of my un-familiarty with the input-box component, but just look at it i don't know right away what segment means

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 dateSegmentObj might not be the best name, but I updated the description for this prop in the TSDoc. Did that help?

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;
};
}
Loading
Loading