-
Notifications
You must be signed in to change notification settings - Fork 72
[LG-5504] feat(input-box): add InputBox
#3285
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
Open
shaneeza
wants to merge
6
commits into
LG-5504/input-box-segment
Choose a base branch
from
LG-5504/input-box-component
base: LG-5504/input-box-segment
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,346
−6
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
09d117b
feat(input-box): enhance InputBox and InputSegment components with ne…
shaneeza 35d975b
feat(input-box): add '@leafygreen-ui/a11y' as a dependency in pnpm-lo…
shaneeza 2f81c18
fix(input-box): fix lint errors
shaneeza 86fbca9
feat(input-box): set default size for InputBox in stories and refacto…
shaneeza 46746a5
Merge branch LG-5504/input-box-context of github.com:mongodb/leafygre…
shaneeza 6be4fdf
Merge branch LG-5504/input-box-segment of github.com:mongodb/leafygre…
shaneeza File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,130 @@ | ||
| # Internal Input Box | ||
| # Input Box | ||
|
|
||
| An internal component intended to be used by any date or time component. | ||
| I.e. `DatePicker`, `TimeInput` etc. | ||
|  | ||
|
|
||
| ## 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 | ||
| const MySegment = ({ segment, ...props }) => ( | ||
| <InputSegment | ||
| segment={segment} | ||
| min={minValues[segment]} | ||
| max={maxValues[segment]} | ||
| {...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' }, | ||
| ]} | ||
| charsPerSegment={{ day: 2, month: 2, year: 4 }} | ||
| segmentRefs={{ day: dayRef, month: monthRef, year: yearRef }} | ||
| segmentRules={{ | ||
| day: { maxChars: 2, minExplicitValue: 1 }, | ||
| month: { maxChars: 2, minExplicitValue: 4 }, | ||
| year: { maxChars: 4, minExplicitValue: 1970 }, | ||
| }} | ||
| disabled={false} | ||
| size={Size.Default} | ||
| />; | ||
| ``` | ||
|
|
||
| Refer to `DateInputBox` in the `@leafygreen-ui/date-picker` package for an implementation example. | ||
|
|
||
| ## Overview | ||
|
|
||
| An internal component intended to be used by any date or time component, such as `DatePicker`, `TimeInput`, etc. | ||
|
|
||
| This package provides two main components that work together to create segmented input experiences. | ||
|
|
||
| ### InputBox | ||
|
|
||
| A generic controlled input box component that renders an input with multiple segments separated by literals. | ||
|
|
||
| **Key Features:** | ||
|
|
||
| - **Auto-format**: Automatically pads segment values with leading zeros (based on `charsPerSegment`) when they become explicit/unambiguous. A value is explicit when it either: (1) reaches the maximum character length, or (2) meets or exceeds the `minExplicitValue` threshold (e.g., typing "5" for day → "05", but typing "2" stays "2" since it could be 20-29). Also formats on blur. | ||
| - **Auto-focus**: Automatically advances focus to the next segment when the current segment is complete | ||
| - **Keyboard navigation**: Handles left/right arrow key navigation between segments | ||
| - **Segment management**: Renders segments and separators based on `formatParts` (from `Intl.DateTimeFormat`) | ||
|
|
||
| The component handles high-level interactions like moving between segments, while delegating segment-specific logic to the `InputSegment` component. Internally, it uses `InputBoxContext` to share state and handlers across all segments. | ||
|
|
||
| #### Props | ||
|
|
||
| | Prop | Type | Description | Default | | ||
| | ------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | | ||
| | `segments` | `Record<Segment, string>` | An object containing the values of the segments.<br/><br/>Example: `{ day: '01', month: '02', year: '2025' }` | | | ||
| | `setSegment` | `(segment: Segment, value: string) => void` | A function that sets the value of a segment.<br/><br/>Example: `(segment: 'day', value: '15') => void` | | | ||
| | `segmentEnum` | `Record<string, Segment>` | An enumerable object that maps the segment names to their values.<br/><br/>Example: `{ Day: 'day', Month: 'month', Year: 'year' }` | | | ||
| | `segmentComponent` | `React.ComponentType<InputSegmentComponentProps<Segment>>` | React component to render each segment (must accept `InputSegmentComponentProps`).<br/><br/>Example: `DateInputSegment` | | | ||
| | `formatParts` | `Array<Intl.DateTimeFormatPart>` | Array of `Intl.DateTimeFormatPart` defining segment order and separators.<br/><br/>Example:<br/>`[{ type: 'month', value: '02' },`<br/>`{ type: 'literal', value: '/' }, ...]` | | | ||
| | `charsPerSegment` | `Record<Segment, number>` | Record of maximum characters per segment.<br/><br/>Example: `{ day: 2, month: 2, year: 4 }` | | | ||
| | `segmentRefs` | `Record<Segment, ReturnType<DynamicRefGetter<HTMLInputElement>>>` | Record mapping segment names to their input refs.<br/><br/>Example: `{ day: dayRef, month: monthRef, year: yearRef }` | | | ||
| | `segmentRules` | `Record<Segment, ExplicitSegmentRule>` | Record of validation rules per segment with `maxChars` and `minExplicitValue`.<br/><br/>Example:<br/>`{ day: { maxChars: 2, minExplicitValue: 1 },`<br/>`month: { maxChars: 2, minExplicitValue: 4 }, ... }` | | | ||
| | `disabled` | `boolean` | Whether the input is disabled | | | ||
| | `size` | `Size` | Size of the input.<br/><br/>Example: `Size.Default`, `Size.Small`, or `Size.XSmall` | | | ||
| | `onSegmentChange` | `InputSegmentChangeEventHandler<Segment, string>` | Optional callback fired when any segment changes | | | ||
| | `labelledBy` | `string` | ID of the labelling element for accessibility.<br/><br/>Example: `'date-input-label'` | | | ||
|
|
||
| \+ other HTML `div` element props | ||
|
|
||
| ### InputSegment | ||
|
|
||
| A controlled input segment component that renders a single input field within an `InputBox`. | ||
|
|
||
| **Key Features:** | ||
|
|
||
| - **Up/down arrow key navigation**: Increment/decrement segment values using arrow keys | ||
| - **Value validation**: Validates input against configurable min/max ranges | ||
| - **Auto-formatting**: Formats values with leading zeros based on character length | ||
| - **Rollover support**: Optionally rolls over values (e.g., 31 → 1 for days, or stops at boundaries) | ||
| - **Keyboard interaction**: Handles backspace and space keys to clear values | ||
| - **onChange/onBlur events**: Fires custom change events with segment metadata | ||
|
|
||
| #### Props | ||
|
|
||
| | Prop | Type | Description | Default | | ||
| | ---------------------- | --------- | --------------------------------------------------------------------------------------------------------- | ------- | | ||
| | `segment` | `string` | The segment identifier.<br/><br/>Example: `'day'`, `'month'`, or `'year'` | | | ||
| | `min` | `number` | Minimum valid value for the segment.<br/><br/>Example: `1` for day, `1` for month, `1900` for year | | | ||
| | `max` | `number` | Maximum valid value for the segment.<br/><br/>Example: `31` for day, `12` for month, `2100` for year | | | ||
| | `step` | `number` | Increment/decrement step for arrow keys | `1` | | ||
| | `shouldWrap` | `boolean` | Whether values should wrap around at min/max boundaries.<br/><br/>Example: `true` to wrap 31 → 1 for days | | | ||
| | `shouldSkipValidation` | `boolean` | Skips validation for segments that allow extended ranges | | | ||
|
|
||
| \+ native HTML `input` element props |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import React from 'react'; | ||
| import { | ||
| storybookExcludedControlParams, | ||
| StoryMetaType, | ||
| } from '@lg-tools/storybook-utils'; | ||
| import { StoryFn } from '@storybook/react'; | ||
|
|
||
| import { css } from '@leafygreen-ui/emotion'; | ||
| import { palette } from '@leafygreen-ui/palette'; | ||
| import { Size } from '@leafygreen-ui/tokens'; | ||
|
|
||
| import { SegmentObjMock } from './testutils/testutils.mocks'; | ||
| import { InputBox, InputBoxProps } from './InputBox'; | ||
| import { InputBoxWithState } from './testutils'; | ||
|
|
||
| const meta: StoryMetaType<typeof InputBox> = { | ||
| title: 'Components/Inputs/InputBox', | ||
| component: InputBox, | ||
| decorators: [ | ||
| StoryFn => ( | ||
| <div | ||
| className={css` | ||
| border: 1px solid ${palette.gray.base}; | ||
| `} | ||
| > | ||
| <StoryFn /> | ||
| </div> | ||
| ), | ||
| ], | ||
| parameters: { | ||
| default: 'LiveExample', | ||
| controls: { | ||
| exclude: [ | ||
| ...storybookExcludedControlParams, | ||
| 'segments', | ||
| 'segmentObj', | ||
| 'segmentRefs', | ||
| 'setSegment', | ||
| 'charsPerSegment', | ||
| 'formatParts', | ||
| 'segmentRules', | ||
| 'labelledBy', | ||
| 'onSegmentChange', | ||
| 'renderSegment', | ||
| 'segmentComponent', | ||
| 'segmentEnum', | ||
| ], | ||
| }, | ||
| }, | ||
| argTypes: { | ||
| size: { | ||
| control: 'select', | ||
| options: Object.values(Size), | ||
| }, | ||
| }, | ||
| args: { | ||
| size: Size.Default, | ||
| }, | ||
| }; | ||
| export default meta; | ||
|
|
||
| export const LiveExample: StoryFn<typeof InputBox> = props => { | ||
| return ( | ||
| <InputBoxWithState {...(props as Partial<InputBoxProps<SegmentObjMock>>)} /> | ||
| ); | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
nit: should we add a default size via
args?