-
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
Changes from 2 commits
09d117b
35d975b
2f81c18
86fbca9
7e6e4b4
1c69f5d
46746a5
691bde9
b2984f3
6be4fdf
b0d7bba
3986897
2eda96e
fff0557
959c5a1
e97d393
0ab86c9
ad1f017
81a943c
8cfadbe
68fc653
a04d5ec
0101c32
662f2dd
967b33b
a589e94
4a03f0b
e8a3705
4cf138e
a7062e2
dd132ea
0e9b9bd
5e73301
0baa5dc
6db5451
2d76c2c
d4ec60d
bf2eeda
5c05dc1
904fb8c
81e00e6
3792f8b
7b1db76
348faa3
20da919
6942348
3fe8f0b
2dc0134
b4dd84d
f2cfaa3
c269b96
73ea273
a55bf24
717daa7
67d8f9f
d7c1fc2
f52ed19
94f2900
68e5f2c
3896c9c
410813e
ed31fdc
adaa3b6
a106f71
df546c1
ea4d8b8
4d1030b
f342d2f
f7f28eb
349b655
bdabf2a
c666bb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| 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 { SegmentObjMock } from './testutils/testutils.mocks'; | ||
| import { InputBox, InputBoxProps } from './InputBox'; | ||
| import { InputBoxWithState } from './testutils'; | ||
| import { Size } from '@leafygreen-ui/tokens'; | ||
|
|
||
| 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', | ||
| ], | ||
| }, | ||
| }, | ||
shaneeza marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| argTypes: { | ||
| size: { | ||
| control: 'select', | ||
| options: Object.values(Size), | ||
| }, | ||
| }, | ||
| }; | ||
| export default meta; | ||
|
|
||
| export const LiveExample: StoryFn<typeof InputBox> = props => { | ||
| return ( | ||
| <InputBoxWithState {...(props as Partial<InputBoxProps<SegmentObjMock>>)} /> | ||
| ); | ||
| }; | ||
|
Comment on lines
+94
to
+98
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I missed this comment, but I added it! |
||
Uh oh!
There was an error while loading. Please reload this page.