Skip to content

Commit 7b57fe4

Browse files
authored
[LG-5504] feat(input-box): add InputBox (#3285)
* feat(input-box): enhance InputBox and InputSegment components with new features and documentation. * feat(input-box): add '@leafygreen-ui/a11y' as a dependency in pnpm-lock.yaml * fix(input-box): fix lint errors * feat(input-box): set default size for InputBox in stories and refactor InputSegment styles for improved class handling * feat(input-box): implement InputBoxContext and InputBoxProvider with associated types and tests * remove segement files * feat(input-box): implement InputSegment component with styles, tests, and stories * feat(input-box): add @leafygreen-ui/a11y dependency and update InputSegment component references * refactor(input-box): update createExplicitSegmentValidator tests to use object parameter format for improved clarity and consistency * test(input-box): refactor InputBoxContext tests for improved readability by destructuring context values * refactor(input-box): update InputBoxContext types to extend SharedInputBoxTypes and remove deprecated InputSegment types * fix(input-box): correct comment formatting in testutils.mocks.ts for clarity * feat(input-box): add InputSegment component for modular input handling * feat(input-box): add placeholder for InputSegment types * refactor(input-box): move InputSegmentChangeEventHandler import to shared types for better organization * refactor(input-box): rename min and max props to minSegmentValue and maxSegmentValue for consistency * refactor(input-box): simplify placeholder logic in InputSegment stories using defaultPlaceholderMock * refactor(input-box): update InputSegment styles to use dynamic theme styles and improve organization * feat(input-box): extend InputSegmentProps to include hours, minutes, and seconds segments * refactor(input-box): rename onChange and onBlur props in InputSegment to improve clarity * refactor(input-box): rename shouldSkipValidation prop to shouldValidate for clarity and consistency * refactor(input-box): reorganize imports in testutils for better clarity and structure * refactor(input-box): remove deprecated InputSegment types and update imports in InputBoxContext * refactor(input-box): update InputSegmentChangeEventHandler import to use type alias from shared.types * refactor(input-box): enhance InputSegment types and documentation, adding isInputSegment utility and improving component descriptions * refactor(input-box): streamline InputSegment exports by removing unused types * test(input-box): add accessibility test for InputSegment to ensure no violations when tooltip is closed * refactor(input-box): update InputSegment to remove size prop and enhance type definitions for better clarity * refactor(input-box): enhance InputSegment types by adding onChange and onBlur event handlers with detailed documentation * refactor(input-box): update InputSegment types to extend from 'div' and include additional props for improved functionality * refactor(input-box): simplify SharedInputBoxTypes by removing redundant properties and enhancing type clarity * refactor(input-box): remove InputBoxContext and related tests to streamline input box functionality * refactor(input-box): simplify InputSegment types by removing Value generic and updating related components for improved clarity * refactor(input-box): update InputSegment and InputBox types to include value prop and streamline segment handling * refactor(input-box): update InputSegment types to include value prop and remove unused segmentRefs and segments properties for improved clarity * refactor(input-box): remove unused Size import from InputBox.spec.tsx for cleaner code * refactor(input-box): enhance InputBox and InputSegment documentation, update props for clarity, and streamline type exports * testing * refactor(input-box): remove unused dependencies and update InputSegment types for consistency * update lock file * testing * testing build * testing build * test(input-segment): add test for resetting value with complete zeros and update InputSegment story with segmentEnum * refactor(input-box): update separator literal styles to use new token-based approach * fix(input-segment): add missing line to check for number input handling * refactor(input-segment): update comments and variable name for clarity in digit input handling * refactor(input-box): update comment to reflect correct component responsible for increment/decrement logic * refactor(input-segment): utilize isSingleDigit utility for digit input handling * refactor(input-box): enhance documentation for InputBox component to clarify functionality and usage * refactor(input-box): integrate size prop into InputBox and InputSegment components for enhanced customization * refactor(input-box): migrate Size import to shared.types for consistent usage across components * refactor(input-box): enhance InputBox and InputSegment tests with segmentRefs integration for improved focus handling * feat(input-box): add comprehensive mocks for date and time segments in testutils for enhanced testing capabilities * feat(input-box): integrate lodash for utility functions and enhance InputBox stories with date and time segment examples * refactor(input-box): remove unused props from InputBox stories and testutils for cleaner code * fix(input-box): ensure segments prop is required and handle error logging in InputBox component * refactor(input-box): rename isSingleDigit to isSingleDigitKey and update README * refactor(input-box): update README and InputSegment to use charsCount instead of charsPerSegment * refactor(input-box): update useSegmentRefs to accept segmentRefs and improve test cases for segment handling
1 parent 4059f91 commit 7b57fe4

24 files changed

+2070
-135
lines changed

packages/input-box/README.md

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,169 @@
1-
# Internal Input Box
1+
# Input Box
22

3-
An internal component intended to be used by any date or time component.
4-
I.e. `DatePicker`, `TimeInput` etc.
3+
![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/input-box.svg)
4+
5+
## Installation
6+
7+
### PNPM
8+
9+
```shell
10+
pnpm add @leafygreen-ui/input-box
11+
```
12+
13+
### Yarn
14+
15+
```shell
16+
yarn add @leafygreen-ui/input-box
17+
```
18+
19+
### NPM
20+
21+
```shell
22+
npm install @leafygreen-ui/input-box
23+
```
24+
25+
## Example
26+
27+
```tsx
28+
import { InputBox, InputSegment } from '@leafygreen-ui/input-box';
29+
import { Size } from '@leafygreen-ui/tokens';
30+
31+
// 1. Create a custom segment component
32+
// InputBox will pass: segment, value, onChange, onBlur, segmentEnum, disabled, ref, aria-labelledby
33+
// You add: minSegmentValue, maxSegmentValue, charsCount, size, and any other InputSegment props
34+
const MySegment = ({
35+
segment,
36+
value,
37+
onChange,
38+
onBlur,
39+
segmentEnum,
40+
disabled,
41+
...props
42+
}) => (
43+
<InputSegment
44+
segment={segment}
45+
value={value}
46+
onChange={onChange}
47+
onBlur={onBlur}
48+
segmentEnum={segmentEnum}
49+
disabled={disabled}
50+
minSegmentValue={minValues[segment]}
51+
maxSegmentValue={maxValues[segment]}
52+
charsCount={charsPerSegment[segment]}
53+
size={Size.Default}
54+
{...props}
55+
/>
56+
);
57+
58+
// 2. Use InputBox with your segments
59+
<InputBox
60+
segments={{ day: '01', month: '02', year: '2025' }}
61+
setSegment={(segment, value) => console.log(segment, value)}
62+
segmentEnum={{ Day: 'day', Month: 'month', Year: 'year' }}
63+
segmentComponent={MySegment}
64+
formatParts={[
65+
{ type: 'month', value: '02' },
66+
{ type: 'literal', value: '/' },
67+
{ type: 'day', value: '01' },
68+
{ type: 'literal', value: '/' },
69+
{ type: 'year', value: '2025' },
70+
]}
71+
segmentRefs={{ day: dayRef, month: monthRef, year: yearRef }}
72+
segmentRules={{
73+
day: { maxChars: 2, minExplicitValue: 4 },
74+
month: { maxChars: 2, minExplicitValue: 2 },
75+
year: { maxChars: 4, minExplicitValue: 1970 },
76+
}}
77+
disabled={false}
78+
/>;
79+
```
80+
81+
Refer to `DateInputBox` in the `@leafygreen-ui/date-picker` package for a full implementation example.
82+
83+
## Overview
84+
85+
An internal component for building date or time inputs with multiple segments (e.g., `DatePicker`, `TimeInput`).
86+
87+
### How It Works
88+
89+
`InputBox` handles the high-level coordination (navigation, formatting, focus management), while `InputSegment` handles individual segment behavior (validation, arrow key increments).
90+
91+
**The `segmentComponent` Pattern:**
92+
93+
`InputBox` doesn't directly render `InputSegment` components. Instead, you provide a custom `segmentComponent` that acts as a wrapper:
94+
95+
1. **InputBox automatically passes** these props to your `segmentComponent`:
96+
97+
- `segment` - the segment identifier (e.g., `'day'`, `'month'`)
98+
- `value` - the current segment value
99+
- `onChange` - change handler for the segment
100+
- `onBlur` - blur handler for the segment
101+
- `segmentEnum` - the segment enum object
102+
- `disabled` - whether the segment is disabled
103+
- `ref` - ref for the input element
104+
- `aria-labelledby` - accessibility label reference
105+
- `charsCount` - character length
106+
- `size` - input size
107+
108+
2. **Your `segmentComponent` adds** segment-specific configuration:
109+
- `minSegmentValue` / `maxSegmentValue` - validation ranges
110+
- `step`, `shouldWrap`, `shouldValidate` - optional behavior customization
111+
112+
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.
113+
114+
### InputBox
115+
116+
A generic controlled input component that renders multiple segments separated by literals (e.g., `MM/DD/YYYY`).
117+
118+
**Key Features:**
119+
120+
- **Auto-format**: Pads values with leading zeros when explicit (reaches max length or `minExplicitValue` threshold)
121+
- **Auto-advance**: Moves focus to next segment when current segment is complete
122+
- **Keyboard navigation**: Arrow keys move between segments, backspace navigates back when empty
123+
124+
#### Props
125+
126+
| Prop | Type | Description | Default |
127+
| ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------ | ------- |
128+
| `segments` | `Record<Segment, string>` | Current values for all segments | |
129+
| `setSegment` | `(segment: Segment, value: string) => void` | Callback to update a segment's value | |
130+
| `segmentEnum` | `Record<string, Segment>` | Maps segment names to values (e.g., `{ Day: 'day' }`) | |
131+
| `segmentComponent` | `React.ComponentType<InputSegmentComponentProps<Segment>>` | Custom wrapper component that renders InputSegment with segment-specific props | |
132+
| `formatParts` | `Array<Intl.DateTimeFormatPart>` | Defines segment order and separators | |
133+
| `segmentRefs` | `Record<Segment, React.RefObject<HTMLInputElement>>` | Refs for each segment input | |
134+
| `segmentRules` | `Record<Segment, ExplicitSegmentRule>` | Rules for auto-formatting (`maxChars`, `minExplicitValue`) | |
135+
| `disabled` | `boolean` | Disables all segments | |
136+
| `onSegmentChange` | `InputSegmentChangeEventHandler<Segment, string>` | Callback fired on any segment change | |
137+
| `labelledBy` | `string` | ID of labelling element for accessibility | |
138+
139+
\+ other HTML `div` props
140+
141+
### InputSegment
142+
143+
A generic controlled input field for a single segment within `InputBox`.
144+
145+
**Key Features:**
146+
147+
- **Arrow key increment/decrement**: Up/down arrows adjust values with optional wrapping
148+
- **Value validation**: Validates against min/max ranges
149+
- **Keyboard shortcuts**: Backspace/Space clears the value
150+
151+
#### Props
152+
153+
| Prop | Type | Description | Default |
154+
| ----------------- | ------------------------------------------------- | -------------------------------------------- | ------- |
155+
| `segment` | `Segment` | Segment identifier (e.g., `'day'`) | |
156+
| `value` | `string` | Current segment value | |
157+
| `minSegmentValue` | `number` | Minimum valid value | |
158+
| `maxSegmentValue` | `number` | Maximum valid value | |
159+
| `charsCount` | `number` | Max character length | |
160+
| `size` | `Size` | Input size | |
161+
| `segmentEnum` | `Record<string, Segment>` | Segment enum from InputBox | |
162+
| `onChange` | `InputSegmentChangeEventHandler<Segment, string>` | Change handler | |
163+
| `onBlur` | `FocusEventHandler<HTMLInputElement>` | Blur handler | |
164+
| `disabled` | `boolean` | Disables the segment | |
165+
| `step` | `number` | Arrow key increment/decrement step | `1` |
166+
| `shouldWrap` | `boolean` | Whether to wrap at boundaries (e.g., 31 → 1) | `true` |
167+
| `shouldValidate` | `boolean` | Whether to validate against min/max | `true` |
168+
169+
\+ native HTML `input` props

packages/input-box/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"@leafygreen-ui/hooks": "workspace:^",
3535
"@leafygreen-ui/date-utils": "workspace:^",
3636
"@leafygreen-ui/tokens": "workspace:^",
37-
"@leafygreen-ui/typography": "workspace:^"
37+
"@leafygreen-ui/typography": "workspace:^",
38+
"lodash": "^4.17.21"
3839
},
3940
"peerDependencies": {
4041
"@leafygreen-ui/leafygreen-provider": "workspace:^"
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/* eslint-disable no-console */
2+
import React from 'react';
3+
import {
4+
storybookExcludedControlParams,
5+
StoryMetaType,
6+
} from '@lg-tools/storybook-utils';
7+
import { StoryFn, StoryObj } from '@storybook/react';
8+
9+
import { css } from '@leafygreen-ui/emotion';
10+
import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';
11+
import { palette } from '@leafygreen-ui/palette';
12+
13+
import {
14+
dateSegmentEmptyMock,
15+
defaultFormatPartsMock,
16+
SegmentObjMock,
17+
segmentRulesMock,
18+
segmentsMock,
19+
timeFormatPartsMock,
20+
TimeInputSegmentWrapper,
21+
TimeSegmentObjMock,
22+
timeSegmentRulesMock,
23+
timeSegmentsEmptyMock,
24+
timeSegmentsMock,
25+
} from './testutils/testutils.mocks';
26+
import { InputBox, InputBoxProps } from './InputBox';
27+
import { Size } from './shared.types';
28+
import { InputBoxWithState, InputSegmentWrapper } from './testutils';
29+
30+
const meta: StoryMetaType<typeof InputBox> = {
31+
title: 'Components/Inputs/InputBox',
32+
component: InputBox,
33+
decorators: [
34+
(StoryFn, context: any) => (
35+
<div
36+
className={css`
37+
border: 1px solid ${palette.gray.base};
38+
`}
39+
>
40+
<LeafyGreenProvider darkMode={context?.args?.darkMode}>
41+
<StoryFn />
42+
</LeafyGreenProvider>
43+
</div>
44+
),
45+
],
46+
parameters: {
47+
default: 'LiveExample',
48+
controls: {
49+
exclude: [
50+
...storybookExcludedControlParams,
51+
'segments',
52+
'segmentObj',
53+
'segmentRefs',
54+
'setSegment',
55+
'formatParts',
56+
'segmentRules',
57+
'labelledBy',
58+
'onSegmentChange',
59+
'renderSegment',
60+
'segmentComponent',
61+
'segmentEnum',
62+
],
63+
},
64+
generate: {
65+
storyNames: ['Date', 'Time'],
66+
combineArgs: {
67+
disabled: [false, true],
68+
size: Object.values(Size),
69+
darkMode: [false, true],
70+
},
71+
decorator: (StoryFn, context) => (
72+
<LeafyGreenProvider darkMode={context?.args.darkMode}>
73+
<StoryFn />
74+
</LeafyGreenProvider>
75+
),
76+
},
77+
},
78+
argTypes: {
79+
disabled: {
80+
control: 'boolean',
81+
},
82+
size: {
83+
control: 'select',
84+
options: Object.values(Size),
85+
},
86+
},
87+
args: {
88+
disabled: false,
89+
size: Size.Default,
90+
},
91+
};
92+
export default meta;
93+
94+
export const LiveExample: StoryFn<typeof InputBox> = props => {
95+
return (
96+
<InputBoxWithState {...(props as Partial<InputBoxProps<SegmentObjMock>>)} />
97+
);
98+
};
99+
LiveExample.parameters = {
100+
chromatic: { disableSnapshot: true },
101+
};
102+
103+
export const Date: StoryObj<InputBoxProps<SegmentObjMock>> = {
104+
parameters: {
105+
generate: {
106+
combineArgs: {
107+
segments: [segmentsMock, dateSegmentEmptyMock],
108+
},
109+
},
110+
},
111+
args: {
112+
formatParts: defaultFormatPartsMock,
113+
segmentRules: segmentRulesMock,
114+
segmentEnum: SegmentObjMock,
115+
setSegment: (segment: SegmentObjMock, value: string) => {
116+
console.log('setSegment', segment, value);
117+
},
118+
disabled: false,
119+
size: Size.Default,
120+
segmentComponent: InputSegmentWrapper,
121+
},
122+
};
123+
124+
export const Time: StoryObj<InputBoxProps<TimeSegmentObjMock>> = {
125+
parameters: {
126+
generate: {
127+
combineArgs: {
128+
segments: [timeSegmentsMock, timeSegmentsEmptyMock],
129+
},
130+
},
131+
},
132+
args: {
133+
formatParts: timeFormatPartsMock,
134+
segmentRules: timeSegmentRulesMock,
135+
segmentEnum: TimeSegmentObjMock,
136+
setSegment: (segment: TimeSegmentObjMock, value: string) => {
137+
console.log('setSegment', segment, value);
138+
},
139+
disabled: false,
140+
size: Size.Default,
141+
segmentComponent: TimeInputSegmentWrapper,
142+
},
143+
};

0 commit comments

Comments
 (0)