Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
4f0dc24
refactor(date-picker): extract reusable logic from DateInputSegment, wip
shaneeza Oct 7, 2025
2771235
refactor(date-picker): extract reusable logic from DateInputSegment, wip
shaneeza Oct 7, 2025
4ebb946
refactor(date-picker): enhance InputSegment and DateInputSegment with…
shaneeza Oct 8, 2025
9f40cff
refactor(date-picker): update InputSegment types and enhance DateInpu…
shaneeza Oct 8, 2025
85351a3
refactor(date-picker): WIP enhance InputBox and DateInput components …
shaneeza Oct 21, 2025
ebdde67
refactor(date-picker): integrate InputBox into DateInputBox for impro…
shaneeza Oct 22, 2025
e19c926
refactor(date-picker): clean up DateInputBox and enhance InputBox typ…
shaneeza Oct 22, 2025
edce7cc
refactor(date-picker): enhance DatePicker components with improved ty…
shaneeza Oct 22, 2025
ad12567
refactor(date-picker): enhance InputSegment and DateInputSegment with…
shaneeza Oct 23, 2025
6be0c01
refactor(input-box): move utils into input-box
shaneeza Oct 23, 2025
97d84d4
refactor(input-box): move utils into input-box
shaneeza Oct 23, 2025
cf44545
refactor(date-picker): integrate InputBox and InputSegment into DateP…
shaneeza Oct 26, 2025
01a4072
refactor(date-picker): migrate utility functions to InputBox and enha…
shaneeza Oct 27, 2025
0d8a827
refactor(date-picker): improve type safety in DateInputSegment and cl…
shaneeza Oct 28, 2025
d62e92f
refactor(input-box): update exports in utils to include new segment v…
shaneeza Oct 28, 2025
9e63f3b
refactor(input-box): add devDependencies for palette and enhance Inpu…
shaneeza Oct 28, 2025
0a33907
refactor(input-box): simplify styling logic in InputBox and InputSegm…
shaneeza Oct 28, 2025
d4bc356
refactor(input-box): enhance type definitions in InputBox and InputSe…
shaneeza Oct 28, 2025
d36b02a
refactor(input-box): improve documentation for InputBox and InputSegm…
shaneeza Oct 28, 2025
dfd04ff
refactor(input-box): update documentation for segmentRefs and segment…
shaneeza Oct 28, 2025
853eea4
refactor(input-box, date-picker): streamline value formatting by upda…
shaneeza Oct 28, 2025
93d2c09
refactor(input-box, date-picker): update utils to allow zero values
shaneeza Oct 29, 2025
d2aa6ff
refactor(input-box, date-picker): introduce shouldSkipValidation flag…
shaneeza Oct 29, 2025
e3066f9
refactor(input-box): enhance renderSegment return type for improved t…
shaneeza Oct 29, 2025
ee819d6
refactor(input-box): improve InputBox tests to verify segment renderi…
shaneeza Oct 29, 2025
3eb786c
refactor(input-box): remove unused getLgIds utility and clean up Inpu…
shaneeza Oct 29, 2025
ec62658
refactor(input-box, date-picker): rename segmentObj to segmentEnum fo…
shaneeza Oct 29, 2025
d192456
refactor(input-box, date-picker): update type annotations and enhance…
shaneeza Oct 29, 2025
fb7837a
refactor(input-box): update README and improve InputBox documentation…
shaneeza Oct 29, 2025
40a106d
feat(input-box): adds input-box package and utils
shaneeza Oct 29, 2025
a773e7a
merge conflict
shaneeza Oct 29, 2025
2f600d9
refactor(input-box): consolidate InputBox stories into a single file …
shaneeza Oct 29, 2025
b8d410a
refactor(date-picker): remove input-box dependency and streamline dat…
shaneeza Oct 29, 2025
95de319
feat(date-picker): integrate input-box for date segment handling and …
shaneeza Oct 29, 2025
d7853dc
refactor(date-picker, input-box): implement context for segment manag…
shaneeza Nov 1, 2025
cae12d5
refactor(input-box): update InputBox and InputSegment components to u…
shaneeza Nov 2, 2025
b24d5bc
refactor(input-box): clarify type handling in InputBoxContext with de…
shaneeza Nov 2, 2025
b575531
refactor(input-box): enhance type handling in InputBox and InputSegme…
shaneeza Nov 2, 2025
8236c8d
refactor(input-box): update InputSegment and InputBox components to u…
shaneeza Nov 3, 2025
f3125b6
refactor(date-picker): implement DateInputBoxContext for improved sta…
shaneeza Nov 3, 2025
c40ad4c
refactor(date-picker, input-box): enhance DateInput components by int…
shaneeza Nov 3, 2025
8a76a79
refactor(date-picker, input-box): improve DateInputSegment tests and …
shaneeza Nov 4, 2025
2141a34
refactor(date-picker): reorganize DateInputBox context imports and re…
shaneeza Nov 4, 2025
8e4ada0
docs(input-box): expand README with detailed component descriptions, …
shaneeza Nov 4, 2025
cb9ec7f
docs(input-box): update README to reflect changes in component struct…
shaneeza Nov 4, 2025
015cf67
refactor(input-box): rename `shouldRollover` to `shouldWrap` for clar…
shaneeza Nov 4, 2025
4583b57
refactor(date-picker, input-box): reorganize imports and enhance prop…
shaneeza Nov 4, 2025
d552743
test(input-box): add comprehensive tests for segment navigation and r…
shaneeza Nov 4, 2025
941e93c
fix(input-box, date-picker): address validation and formatting issues…
shaneeza Nov 5, 2025
491ae48
refactor(input-box, date-picker): remove defaultMin prop and enhance …
shaneeza Nov 5, 2025
2ed06d6
refactor(input-box): standardize parameter naming from `allowsZero` t…
shaneeza Nov 5, 2025
15450a5
refactor(input-box): enhance createExplicitSegmentValidator documenta…
shaneeza Nov 5, 2025
94306ec
test(input-box): enhance mouse and keyboard interaction tests for seg…
shaneeza Nov 5, 2025
34ab4e6
test(input-box): add tests for Up and Down arrow key interactions to …
shaneeza Nov 6, 2025
762bc65
refactor(input-box, date-picker): streamline InputBoxContext structur…
shaneeza Nov 6, 2025
80c6c68
Merge branch 'shaneeza/time-picker-integration' of github.com:mongodb…
shaneeza Nov 6, 2025
e3f5a34
docs(input-box): update README.md to enhance installation instruction…
shaneeza Nov 6, 2025
3074847
Merge branch 'LG-5504/input-box-component' of github.com:mongodb/leaf…
shaneeza Nov 6, 2025
3a13648
Merge branch 'LG-5504/input-box-component' of github.com:mongodb/leaf…
shaneeza Nov 6, 2025
e4b54c5
Merge branch 'LG-5504/input-box-component' of github.com:mongodb/leaf…
shaneeza Nov 7, 2025
0fd74e6
refactor(date-picker): simplify ProviderWrapper in DateInputSegment s…
shaneeza Nov 7, 2025
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
5 changes: 5 additions & 0 deletions .changeset/input-box.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/input-box': minor
---

Initial release of `InputBox`
1 change: 1 addition & 0 deletions packages/date-picker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@leafygreen-ui/hooks": "workspace:^",
"@leafygreen-ui/icon": "workspace:^",
"@leafygreen-ui/icon-button": "workspace:^",
"@leafygreen-ui/input-box": "workspace:^",
"@leafygreen-ui/lib": "workspace:^",
"@leafygreen-ui/palette": "workspace:^",
"@leafygreen-ui/popover": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import userEvent from '@testing-library/user-event';

import { Month, newUTC } from '@leafygreen-ui/date-utils';
import { getLgIds as getLgFormFieldIds } from '@leafygreen-ui/form-field';
import { getValueFormatter } from '@leafygreen-ui/input-box';
import { eventContainingTargetValue } from '@leafygreen-ui/testing-lib';

import { DateSegment } from '../shared';
import { defaultMax, defaultMin } from '../shared/constants';
import { charsPerSegment, defaultMax, defaultMin } from '../shared/constants';
import {
getFormattedDateString,
getFormattedSegmentsFromDate,
getValueFormatter,
} from '../shared/utils';

import {
Expand Down Expand Up @@ -79,7 +79,9 @@ describe('DatePicker keyboard interaction', () => {

const segmentCases = ['year', 'month', 'day'] as Array<DateSegment>;
describe.each(segmentCases)('%p segment', segment => {
const formatter = getValueFormatter(segment);
const formatter = getValueFormatter({
charsPerSegment: charsPerSegment[segment],
});
/** Utility only for this suite. Returns the day|month|year element from the render result */
const getRelevantInput = (renderResult: RenderDatePickerResult) =>
segment === 'year'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {
import isNull from 'lodash/isNull';

import { isInvalidDateObject, isSameUTCDay } from '@leafygreen-ui/date-utils';
import { isElementInputSegment } from '@leafygreen-ui/input-box';
import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib';

import {
Expand All @@ -17,11 +18,7 @@ import {
} from '../../shared/components/DateInput';
import { DateInputSegmentChangeEventHandler } from '../../shared/components/DateInput/DateInputSegment';
import { useSharedDatePickerContext } from '../../shared/context';
import {
getFormattedDateStringFromSegments,
getRelativeSegmentRef,
isElementInputSegment,
} from '../../shared/utils';
import { getFormattedDateStringFromSegments } from '../../shared/utils';
import { useDatePickerContext } from '../DatePickerContext';
import { getSegmentToFocus } from '../utils/getSegmentToFocus';

Expand Down Expand Up @@ -110,77 +107,11 @@ export const DatePickerInput = forwardRef<HTMLDivElement, DatePickerInputProps>(
// if target is not a segment, do nothing
if (!isSegment) return;

const isSegmentEmpty = !target.value;

switch (key) {
case keyMap.ArrowLeft: {
// Without this, the input ignores `.select()`
e.preventDefault();
// if input is empty,
// set focus to prev input (if it exists)
const segmentToFocus = getRelativeSegmentRef('prev', {
segment: target,
formatParts,
segmentRefs,
});

segmentToFocus?.current?.focus();
segmentToFocus?.current?.select();
// otherwise, use default behavior

break;
}

case keyMap.ArrowRight: {
// Without this, the input ignores `.select()`
e.preventDefault();
// if input is empty,
// set focus to next. input (if it exists)
const segmentToFocus = getRelativeSegmentRef('next', {
segment: target,
formatParts,
segmentRefs,
});

segmentToFocus?.current?.focus();
segmentToFocus?.current?.select();
// otherwise, use default behavior

break;
}

case keyMap.ArrowUp:
case keyMap.ArrowDown: {
// increment/decrement logic implemented by DateInputSegment
break;
}

case keyMap.Backspace: {
if (isSegmentEmpty) {
// prevent the backspace in the previous segment
e.preventDefault();

const segmentToFocus = getRelativeSegmentRef('prev', {
segment: target,
formatParts,
segmentRefs,
});
segmentToFocus?.current?.focus();
segmentToFocus?.current?.select();
}
break;
}

case keyMap.Space: {
openMenu();
break;
}

case keyMap.Enter:
case keyMap.Escape:
case keyMap.Tab:
// Behavior handled by parent or menu
break;
}

// call any handler that was passed in
Expand Down Expand Up @@ -232,17 +163,17 @@ export const DatePickerInput = forwardRef<HTMLDivElement, DatePickerInputProps>(
<DateFormField
ref={fwdRef}
buttonRef={calendarButtonRef}
onKeyDown={handleInputKeyDown}
onInputClick={handleInputClick}
onBlur={handleInputBlur}
onIconButtonClick={handleIconButtonClick}
onBlur={handleInputBlur}
{...rest}
>
<DateInputBox
value={value}
setValue={handleInputValueChange}
segmentRefs={segmentRefs}
onSegmentChange={handleSegmentChange}
onKeyDown={handleInputKeyDown}
/>
</DateFormField>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FocusEventHandler, useEffect } from 'react';
import React, { useEffect } from 'react';
import isEqual from 'lodash/isEqual';
import isNull from 'lodash/isNull';

Expand All @@ -7,37 +7,20 @@ import {
isInvalidDateObject,
isValidDate,
} from '@leafygreen-ui/date-utils';
import { cx } from '@leafygreen-ui/emotion';
import { useForwardedRef } from '@leafygreen-ui/hooks';
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider';
import { keyMap } from '@leafygreen-ui/lib';
import { InputBox } from '@leafygreen-ui/input-box';

import { charsPerSegment, dateSegmentRules } from '../../../constants';
import { useSharedDatePickerContext } from '../../../context';
import { useDateSegments } from '../../../hooks';
import { DateSegment, DateSegmentsState } from '../../../types';
import {
DateSegment,
DateSegmentsState,
DateSegmentValue,
isDateSegment,
} from '../../../types';
import {
getMaxSegmentValue,
getMinSegmentValue,
getRelativeSegment,
getValueFormatter,
isEverySegmentFilled,
isEverySegmentValueExplicit,
isExplicitSegmentValue,
newDateFromSegments,
} from '../../../utils';
import { DateInputBoxProvider } from '../DateInputBoxContext';
import { DateInputSegment } from '../DateInputSegment';
import { DateInputSegmentChangeEventHandler } from '../DateInputSegment/DateInputSegment.types';

import {
segmentPartsWrapperStyles,
separatorLiteralDisabledStyles,
separatorLiteralStyles,
} from './DateInputBox.styles';
import { DateInputBoxProps } from './DateInputBox.types';

/**
Expand All @@ -62,25 +45,13 @@ export const DateInputBox = React.forwardRef<HTMLDivElement, DateInputBoxProps>(
labelledBy,
segmentRefs,
onSegmentChange,
onKeyDown,
...rest
}: DateInputBoxProps,
fwdRef,
) => {
const { isDirty, formatParts, disabled, min, max, setIsDirty } =
const { isDirty, formatParts, disabled, setIsDirty, size } =
useSharedDatePickerContext();
const { theme } = useDarkMode();

const containerRef = useForwardedRef(fwdRef, null);

/** Formats and sets the segment value */
const getFormattedSegmentValue = (
segmentName: DateSegment,
segmentValue: DateSegmentValue,
): DateSegmentValue => {
const formatter = getValueFormatter(segmentName);
const formattedValue = formatter(segmentValue);
return formattedValue;
};

/** if the value is a `Date` the component is dirty */
useEffect(() => {
Expand Down Expand Up @@ -118,92 +89,32 @@ export const DateInputBox = React.forwardRef<HTMLDivElement, DateInputBoxProps>(
}
};

/** State Management for segments using a useReducer instead of useState */
/** Keep track of each date segment */
const { segments, setSegment } = useDateSegments(value, {
onUpdate: handleSegmentUpdate,
});

/** Fired when an individual segment value changes */
const handleSegmentInputChange: DateInputSegmentChangeEventHandler =
segmentChangeEvent => {
let segmentValue = segmentChangeEvent.value;
const { segment: segmentName, meta } = segmentChangeEvent;
const changedViaArrowKeys =
meta?.key === keyMap.ArrowDown || meta?.key === keyMap.ArrowUp;

// Auto-format the segment if it is explicit and was not changed via arrow-keys
if (
!changedViaArrowKeys &&
isExplicitSegmentValue(segmentName, segmentValue)
) {
segmentValue = getFormattedSegmentValue(segmentName, segmentValue);

// Auto-advance focus (if possible)
const nextSegmentName = getRelativeSegment('next', {
segment: segmentName,
formatParts,
});

if (nextSegmentName) {
const nextSegmentRef = segmentRefs[nextSegmentName];
nextSegmentRef?.current?.focus();
nextSegmentRef?.current?.select();
}
}

setSegment(segmentName, segmentValue);
onSegmentChange?.(segmentChangeEvent);
};

/** Triggered when a segment is blurred */
const handleSegmentInputBlur: FocusEventHandler<HTMLInputElement> = e => {
const segmentName = e.target.getAttribute('id');
const segmentValue = e.target.value;

if (isDateSegment(segmentName)) {
const formattedValue = getFormattedSegmentValue(
segmentName,
segmentValue,
);
setSegment(segmentName, formattedValue);
}
};

return (
<div
className={cx(segmentPartsWrapperStyles, className)}
ref={containerRef}
{...rest}
>
{formatParts?.map((part, i) => {
if (part.type === 'literal') {
return (
<span
className={cx(separatorLiteralStyles, {
[separatorLiteralDisabledStyles[theme]]: disabled,
})}
key={'literal-' + i}
>
{part.value}
</span>
);
} else if (isDateSegment(part.type)) {
return (
<DateInputSegment
key={part.type}
ref={segmentRefs[part.type]}
aria-labelledby={labelledBy}
min={getMinSegmentValue(part.type, { date: value, min })}
max={getMaxSegmentValue(part.type, { date: value, max })}
segment={part.type}
value={segments[part.type]}
onChange={handleSegmentInputChange}
onBlur={handleSegmentInputBlur}
/>
);
}
})}
</div>
<DateInputBoxProvider value={value}>
<InputBox
ref={fwdRef}
onKeyDown={onKeyDown}
segmentRefs={segmentRefs}
segmentEnum={DateSegment}
charsPerSegment={charsPerSegment}
formatParts={formatParts}
segments={segments}
setSegment={setSegment}
disabled={disabled}
segmentRules={dateSegmentRules}
onSegmentChange={onSegmentChange}
labelledBy={labelledBy}
segmentComponent={DateInputSegment}
size={size}
{...rest}
/>
</DateInputBoxProvider>
);
},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import { isReact17, renderHook } from '@leafygreen-ui/testing-lib';

import {
DateInputBoxProvider,
useDateInputBoxContext,
} from './DateInputBoxContext';

describe('DateInputBoxContext', () => {
test('throws error when used outside of DateInputBoxProvider', () => {
/**
* The version of `renderHook` imported from "@testing-library/react-hooks", (used in React 17)
* has an error boundary, and doesn't throw errors as expected:
* https://github.com/testing-library/react-hooks-testing-library/blob/main/src/index.ts#L5
* */
if (isReact17()) {
const { result } = renderHook(() => useDateInputBoxContext());
expect(result.error.message).toEqual(
'useDateInputBoxContext must be used within a DateInputBoxProvider',
);
} else {
expect(() => renderHook(() => useDateInputBoxContext())).toThrow(
'useDateInputBoxContext must be used within a DateInputBoxProvider',
);
}
});

test('provides context values that match the props passed to the provider', () => {
const value = new Date();
const { result } = renderHook(() => useDateInputBoxContext(), {
wrapper: ({ children }) => (
<DateInputBoxProvider value={value}>{children}</DateInputBoxProvider>
),
});
expect(result.current.value).toEqual(value);
});
});
Loading
Loading