Skip to content
Merged
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
09d117b
feat(input-box): enhance InputBox and InputSegment components with ne…
shaneeza Nov 6, 2025
35d975b
feat(input-box): add '@leafygreen-ui/a11y' as a dependency in pnpm-lo…
shaneeza Nov 6, 2025
2f81c18
fix(input-box): fix lint errors
shaneeza Nov 6, 2025
86fbca9
feat(input-box): set default size for InputBox in stories and refacto…
shaneeza Nov 6, 2025
7e6e4b4
feat(input-box): implement InputBoxContext and InputBoxProvider with …
shaneeza Nov 7, 2025
1c69f5d
remove segement files
shaneeza Nov 7, 2025
46746a5
Merge branch LG-5504/input-box-context of github.com:mongodb/leafygre…
shaneeza Nov 7, 2025
691bde9
feat(input-box): implement InputSegment component with styles, tests,…
shaneeza Nov 7, 2025
b2984f3
feat(input-box): add @leafygreen-ui/a11y dependency and update InputS…
shaneeza Nov 7, 2025
6be4fdf
Merge branch LG-5504/input-box-segment of github.com:mongodb/leafygre…
shaneeza Nov 7, 2025
b0d7bba
refactor(input-box): update createExplicitSegmentValidator tests to u…
shaneeza Nov 7, 2025
3986897
test(input-box): refactor InputBoxContext tests for improved readabil…
shaneeza Nov 7, 2025
2eda96e
refactor(input-box): update InputBoxContext types to extend SharedInp…
shaneeza Nov 10, 2025
fff0557
fix(input-box): correct comment formatting in testutils.mocks.ts for …
shaneeza Nov 10, 2025
959c5a1
feat(input-box): add InputSegment component for modular input handling
shaneeza Nov 10, 2025
e97d393
feat(input-box): add placeholder for InputSegment types
shaneeza Nov 10, 2025
0ab86c9
Merge branch 'LG-5504/input-box-context' of github.com:mongodb/leafyg…
shaneeza Nov 10, 2025
ad1f017
refactor(input-box): move InputSegmentChangeEventHandler import to sh…
shaneeza Nov 10, 2025
81a943c
refactor(input-box): rename min and max props to minSegmentValue and …
shaneeza Nov 10, 2025
8cfadbe
refactor(input-box): simplify placeholder logic in InputSegment stori…
shaneeza Nov 10, 2025
68fc653
refactor(input-box): update InputSegment styles to use dynamic theme …
shaneeza Nov 10, 2025
a04d5ec
feat(input-box): extend InputSegmentProps to include hours, minutes, …
shaneeza Nov 10, 2025
0101c32
refactor(input-box): rename onChange and onBlur props in InputSegment…
shaneeza Nov 10, 2025
662f2dd
refactor(input-box): rename shouldSkipValidation prop to shouldValida…
shaneeza Nov 10, 2025
967b33b
refactor(input-box): reorganize imports in testutils for better clari…
shaneeza Nov 11, 2025
a589e94
refactor(input-box): remove deprecated InputSegment types and update …
shaneeza Nov 11, 2025
4a03f0b
Merge branch 'LG-5504/input-box-context' of github.com:mongodb/leafyg…
shaneeza Nov 11, 2025
e8a3705
refactor(input-box): update InputSegmentChangeEventHandler import to …
shaneeza Nov 11, 2025
4cf138e
refactor(input-box): enhance InputSegment types and documentation, ad…
shaneeza Nov 11, 2025
a7062e2
refactor(input-box): streamline InputSegment exports by removing unus…
shaneeza Nov 11, 2025
dd132ea
test(input-box): add accessibility test for InputSegment to ensure no…
shaneeza Nov 11, 2025
0e9b9bd
refactor(input-box): update InputSegment to remove size prop and enha…
shaneeza Nov 12, 2025
5e73301
refactor(input-box): enhance InputSegment types by adding onChange an…
shaneeza Nov 12, 2025
0baa5dc
refactor(input-box): pull the latest from input-segment
shaneeza Nov 12, 2025
6db5451
refactor(input-box): update InputSegment types to extend from 'div' a…
shaneeza Nov 12, 2025
2d76c2c
Merge branch 'LG-5504/input-box-segment' of github.com:mongodb/leafyg…
shaneeza Nov 12, 2025
d4ec60d
refactor(input-box): simplify SharedInputBoxTypes by removing redunda…
shaneeza Nov 12, 2025
bf2eeda
refactor(input-box): remove InputBoxContext and related tests to stre…
shaneeza Nov 12, 2025
5c05dc1
refactor(input-box): remove InputBoxContext and related types to stre…
shaneeza Nov 12, 2025
904fb8c
refactor(input-box): simplify InputSegment types by removing Value ge…
shaneeza Nov 12, 2025
81e00e6
Merge branch 'LG-5504/input-box-segment' of github.com:mongodb/leafyg…
shaneeza Nov 12, 2025
3792f8b
refactor(input-box): update InputSegment and InputBox types to includ…
shaneeza Nov 12, 2025
7b1db76
refactor(input-box): update InputSegment types to include value prop …
shaneeza Nov 12, 2025
348faa3
Merge branch 'LG-5504/input-box-segment' of github.com:mongodb/leafyg…
shaneeza Nov 12, 2025
20da919
refactor(input-box): remove unused Size import from InputBox.spec.tsx…
shaneeza Nov 12, 2025
6942348
refactor(input-box): enhance InputBox and InputSegment documentation,…
shaneeza Nov 12, 2025
3fe8f0b
Merge branch 'shaneeza/time-picker-integration' of github.com:mongodb…
shaneeza Nov 13, 2025
2dc0134
testing
shaneeza Nov 13, 2025
b4dd84d
refactor(input-box): remove unused dependencies and update InputSegme…
shaneeza Nov 13, 2025
f2cfaa3
update lock file
shaneeza Nov 13, 2025
c269b96
testing
shaneeza Nov 13, 2025
73ea273
testing build
shaneeza Nov 13, 2025
a55bf24
testing build
shaneeza Nov 13, 2025
717daa7
Merge branch 'LG-5504/input-box-segment' of github.com:mongodb/leafyg…
shaneeza Nov 13, 2025
67d8f9f
test(input-segment): add test for resetting value with complete zeros…
shaneeza Nov 13, 2025
d7c1fc2
refactor(input-box): update separator literal styles to use new token…
shaneeza Nov 13, 2025
f52ed19
fix(input-segment): add missing line to check for number input handling
shaneeza Nov 13, 2025
94f2900
Merge branch shaneeza/time-picker-integration of github.com:mongodb/l…
shaneeza Nov 14, 2025
68e5f2c
refactor(input-segment): update comments and variable name for clarit…
shaneeza Nov 14, 2025
3896c9c
refactor(input-box): update comment to reflect correct component resp…
shaneeza Nov 14, 2025
410813e
refactor(input-segment): utilize isSingleDigit utility for digit inpu…
shaneeza Nov 14, 2025
ed31fdc
refactor(input-box): enhance documentation for InputBox component to …
shaneeza Nov 14, 2025
adaa3b6
refactor(input-box): integrate size prop into InputBox and InputSegme…
shaneeza Nov 14, 2025
a106f71
refactor(input-box): migrate Size import to shared.types for consiste…
shaneeza Nov 14, 2025
df546c1
refactor(input-box): enhance InputBox and InputSegment tests with seg…
shaneeza Nov 14, 2025
ea4d8b8
feat(input-box): add comprehensive mocks for date and time segments i…
shaneeza Nov 17, 2025
4d1030b
feat(input-box): integrate lodash for utility functions and enhance I…
shaneeza Nov 17, 2025
f342d2f
refactor(input-box): remove unused props from InputBox stories and te…
shaneeza Nov 17, 2025
f7f28eb
fix(input-box): ensure segments prop is required and handle error log…
shaneeza Nov 18, 2025
349b655
refactor(input-box): rename isSingleDigit to isSingleDigitKey and upd…
shaneeza Nov 19, 2025
bdabf2a
refactor(input-box): update README and InputSegment to use charsCount…
shaneeza Nov 19, 2025
c666bb7
refactor(input-box): update useSegmentRefs to accept segmentRefs and …
shaneeza Nov 19, 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
171 changes: 168 additions & 3 deletions packages/input-box/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,169 @@
# Internal Input Box
# Input Box

An internal component intended to be used by any date or time component.
I.e. `DatePicker`, `TimeInput` etc.
![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/input-box.svg)

## Installation

### PNPM

```shell
pnpm add @leafygreen-ui/input-box
```

### Yarn

```shell
yarn add @leafygreen-ui/input-box
```

### NPM

```shell
npm install @leafygreen-ui/input-box
```

## Example

```tsx
import { InputBox, InputSegment } from '@leafygreen-ui/input-box';
import { Size } from '@leafygreen-ui/tokens';

// 1. Create a custom segment component
// InputBox will pass: segment, value, onChange, onBlur, segmentEnum, disabled, ref, aria-labelledby
// You add: minSegmentValue, maxSegmentValue, charsCount, size, and any other InputSegment props
const MySegment = ({
segment,
value,
onChange,
onBlur,
segmentEnum,
disabled,
...props
}) => (
<InputSegment
segment={segment}
value={value}
onChange={onChange}
onBlur={onBlur}
segmentEnum={segmentEnum}
disabled={disabled}
minSegmentValue={minValues[segment]}
maxSegmentValue={maxValues[segment]}
charsCount={charsPerSegment[segment]}
size={Size.Default}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This needs to be updated

{...props}
/>
);

// 2. Use InputBox with your segments
<InputBox
segments={{ day: '01', month: '02', year: '2025' }}
setSegment={(segment, value) => console.log(segment, value)}
segmentEnum={{ Day: 'day', Month: 'month', Year: 'year' }}
segmentComponent={MySegment}
formatParts={[
{ type: 'month', value: '02' },
{ type: 'literal', value: '/' },
{ type: 'day', value: '01' },
{ type: 'literal', value: '/' },
{ type: 'year', value: '2025' },
]}
segmentRefs={{ day: dayRef, month: monthRef, year: yearRef }}
segmentRules={{
day: { maxChars: 2, minExplicitValue: 4 },
month: { maxChars: 2, minExplicitValue: 2 },
year: { maxChars: 4, minExplicitValue: 1970 },
}}
disabled={false}
/>;
```

Refer to `DateInputBox` in the `@leafygreen-ui/date-picker` package for a full implementation example.

## Overview

An internal component for building date or time inputs with multiple segments (e.g., `DatePicker`, `TimeInput`).

### How It Works

`InputBox` handles the high-level coordination (navigation, formatting, focus management), while `InputSegment` handles individual segment behavior (validation, arrow key increments).

**The `segmentComponent` Pattern:**

`InputBox` doesn't directly render `InputSegment` components. Instead, you provide a custom `segmentComponent` that acts as a wrapper:

1. **InputBox automatically passes** these props to your `segmentComponent`:

- `segment` - the segment identifier (e.g., `'day'`, `'month'`)
- `value` - the current segment value
- `onChange` - change handler for the segment
- `onBlur` - blur handler for the segment
- `segmentEnum` - the segment enum object
- `disabled` - whether the segment is disabled
- `ref` - ref for the input element
- `aria-labelledby` - accessibility label reference
- `charsCount` - character length
- `size` - input size

2. **Your `segmentComponent` adds** segment-specific configuration:
- `minSegmentValue` / `maxSegmentValue` - validation ranges
- `step`, `shouldWrap`, `shouldValidate` - optional behavior customization

This pattern allows you to define segment-specific rules (like min/max values that vary by segment) while keeping the core InputBox logic generic and reusable.

### InputBox

A generic controlled input component that renders multiple segments separated by literals (e.g., `MM/DD/YYYY`).

**Key Features:**

- **Auto-format**: Pads values with leading zeros when explicit (reaches max length or `minExplicitValue` threshold)
- **Auto-advance**: Moves focus to next segment when current segment is complete
- **Keyboard navigation**: Arrow keys move between segments, backspace navigates back when empty

#### Props

| Prop | Type | Description | Default |
| ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------ | ------- |
| `segments` | `Record<Segment, string>` | Current values for all segments | |
| `setSegment` | `(segment: Segment, value: string) => void` | Callback to update a segment's value | |
| `segmentEnum` | `Record<string, Segment>` | Maps segment names to values (e.g., `{ Day: 'day' }`) | |
| `segmentComponent` | `React.ComponentType<InputSegmentComponentProps<Segment>>` | Custom wrapper component that renders InputSegment with segment-specific props | |
| `formatParts` | `Array<Intl.DateTimeFormatPart>` | Defines segment order and separators | |
| `segmentRefs` | `Record<Segment, React.RefObject<HTMLInputElement>>` | Refs for each segment input | |
| `segmentRules` | `Record<Segment, ExplicitSegmentRule>` | Rules for auto-formatting (`maxChars`, `minExplicitValue`) | |
| `disabled` | `boolean` | Disables all segments | |
| `onSegmentChange` | `InputSegmentChangeEventHandler<Segment, string>` | Callback fired on any segment change | |
| `labelledBy` | `string` | ID of labelling element for accessibility | |

\+ other HTML `div` props

### InputSegment

A generic controlled input field for a single segment within `InputBox`.

**Key Features:**

- **Arrow key increment/decrement**: Up/down arrows adjust values with optional wrapping
- **Value validation**: Validates against min/max ranges
- **Keyboard shortcuts**: Backspace/Space clears the value

#### Props

| Prop | Type | Description | Default |
| ----------------- | ------------------------------------------------- | -------------------------------------------- | ------- |
| `segment` | `Segment` | Segment identifier (e.g., `'day'`) | |
| `value` | `string` | Current segment value | |
| `minSegmentValue` | `number` | Minimum valid value | |
| `maxSegmentValue` | `number` | Maximum valid value | |
| `charsCount` | `number` | Max character length | |
| `size` | `Size` | Input size | |
| `segmentEnum` | `Record<string, Segment>` | Segment enum from InputBox | |
| `onChange` | `InputSegmentChangeEventHandler<Segment, string>` | Change handler | |
| `onBlur` | `FocusEventHandler<HTMLInputElement>` | Blur handler | |
| `disabled` | `boolean` | Disables the segment | |
| `step` | `number` | Arrow key increment/decrement step | `1` |
| `shouldWrap` | `boolean` | Whether to wrap at boundaries (e.g., 31 → 1) | `true` |
| `shouldValidate` | `boolean` | Whether to validate against min/max | `true` |

\+ native HTML `input` props
3 changes: 2 additions & 1 deletion packages/input-box/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"@leafygreen-ui/hooks": "workspace:^",
"@leafygreen-ui/date-utils": "workspace:^",
"@leafygreen-ui/tokens": "workspace:^",
"@leafygreen-ui/typography": "workspace:^"
"@leafygreen-ui/typography": "workspace:^",
"lodash": "^4.17.21"
},
"peerDependencies": {
"@leafygreen-ui/leafygreen-provider": "workspace:^"
Expand Down
143 changes: 143 additions & 0 deletions packages/input-box/src/InputBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* eslint-disable no-console */
import React from 'react';
import {
storybookExcludedControlParams,
StoryMetaType,
} from '@lg-tools/storybook-utils';
import { StoryFn, StoryObj } from '@storybook/react';

import { css } from '@leafygreen-ui/emotion';
import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';
import { palette } from '@leafygreen-ui/palette';

import {
dateSegmentEmptyMock,
defaultFormatPartsMock,
SegmentObjMock,
segmentRulesMock,
segmentsMock,
timeFormatPartsMock,
TimeInputSegmentWrapper,
TimeSegmentObjMock,
timeSegmentRulesMock,
timeSegmentsEmptyMock,
timeSegmentsMock,
} from './testutils/testutils.mocks';
import { InputBox, InputBoxProps } from './InputBox';
import { Size } from './shared.types';
import { InputBoxWithState, InputSegmentWrapper } from './testutils';

const meta: StoryMetaType<typeof InputBox> = {
title: 'Components/Inputs/InputBox',
component: InputBox,
decorators: [
(StoryFn, context: any) => (
<div
className={css`
border: 1px solid ${palette.gray.base};
`}
>
<LeafyGreenProvider darkMode={context?.args?.darkMode}>
<StoryFn />
</LeafyGreenProvider>
</div>
),
],
parameters: {
default: 'LiveExample',
controls: {
exclude: [
...storybookExcludedControlParams,
'segments',
'segmentObj',
'segmentRefs',
'setSegment',
'formatParts',
'segmentRules',
'labelledBy',
'onSegmentChange',
'renderSegment',
'segmentComponent',
'segmentEnum',
],
},
generate: {
storyNames: ['Date', 'Time'],
combineArgs: {
disabled: [false, true],
size: Object.values(Size),
darkMode: [false, true],
},
decorator: (StoryFn, context) => (
<LeafyGreenProvider darkMode={context?.args.darkMode}>
<StoryFn />
</LeafyGreenProvider>
),
},
},
argTypes: {
disabled: {
control: 'boolean',
},
size: {
control: 'select',
options: Object.values(Size),
},
},
args: {
disabled: false,
size: Size.Default,
},
};
export default meta;

export const LiveExample: StoryFn<typeof InputBox> = props => {
return (
<InputBoxWithState {...(props as Partial<InputBoxProps<SegmentObjMock>>)} />
);
};
Comment on lines +94 to +98
Copy link
Collaborator

Choose a reason for hiding this comment

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

similar to InputSegment, should we disable this snapshot and include a generated snapshot with some different combos?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I won't block on it, but I think new stories pair well with their related component code similar to when adding specs with their related code

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 missed this comment, but I added it!

LiveExample.parameters = {
chromatic: { disableSnapshot: true },
};

export const Date: StoryObj<InputBoxProps<SegmentObjMock>> = {
parameters: {
generate: {
combineArgs: {
segments: [segmentsMock, dateSegmentEmptyMock],
},
},
},
args: {
formatParts: defaultFormatPartsMock,
segmentRules: segmentRulesMock,
segmentEnum: SegmentObjMock,
setSegment: (segment: SegmentObjMock, value: string) => {
console.log('setSegment', segment, value);
},
disabled: false,
size: Size.Default,
segmentComponent: InputSegmentWrapper,
},
};

export const Time: StoryObj<InputBoxProps<TimeSegmentObjMock>> = {
parameters: {
generate: {
combineArgs: {
segments: [timeSegmentsMock, timeSegmentsEmptyMock],
},
},
},
args: {
formatParts: timeFormatPartsMock,
segmentRules: timeSegmentRulesMock,
segmentEnum: TimeSegmentObjMock,
setSegment: (segment: TimeSegmentObjMock, value: string) => {
console.log('setSegment', segment, value);
},
disabled: false,
size: Size.Default,
segmentComponent: TimeInputSegmentWrapper,
},
};
Loading
Loading