-
Notifications
You must be signed in to change notification settings - Fork 31
Feat/1261 timepicker #3612
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
Merged
Merged
Feat/1261 timepicker #3612
Changes from 17 commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
89b8f96
started making timepicker
adamhaeger c6328f1
timepicker working
adamhaeger e0023e2
progress
adamhaeger fc8c3ec
styling updates
adamhaeger 19a7af9
keep picker open
adamhaeger 2ecfe3b
updated error strings
adamhaeger ddd5179
changed approach to use segments for easier keyboard control. Added c…
adamhaeger b994597
Merge branch 'main' into feat/1261-timepicker
adamhaeger bb03ce4
bug fix
adamhaeger 14de8b7
scrolling into view
adamhaeger be04c40
keyboard navigation working
adamhaeger 6a1d912
fixed input parsing issue, added tests
adamhaeger 23a476b
refactor
adamhaeger 3fdedfa
fix
adamhaeger 8ec76f2
Fixed feedback from PR code review
adamhaeger ae7e785
Updated tests
adamhaeger ecceadb
cleanup
adamhaeger 32d8585
cleanup
adamhaeger e4edc27
added year to displaydata
adamhaeger e391bf3
Merge branch 'main' into feat/1261-timepicker
adamhaeger 4af9316
fixed test
adamhaeger 8939c81
Merge branch 'main' into feat/1261-timepicker
adamhaeger 8664f7a
wip
adamhaeger 392d284
Merge branch 'main' into feat/1261-timepicker
adamhaeger 086d405
refacor wip
adamhaeger c86cb88
refactor wip
adamhaeger d728a51
refactoring and ui improvements
adamhaeger c364e28
wip
adamhaeger b06caa3
wip
adamhaeger ded753d
wip
adamhaeger c6b0372
cleaned up folder
adamhaeger c012838
Merge branch 'main' into feat/1261-timepicker
adamhaeger ed33d3f
test fix
adamhaeger 05a0b5b
cleanup
adamhaeger bb99f85
cleanup
adamhaeger 8a545cf
more cleanup
adamhaeger 4fc91c9
clean and refactor
adamhaeger c3c9993
cleanup
adamhaeger 9ce3607
reduced use of useCallback
adamhaeger a5729ec
refactors
adamhaeger d504073
Merge branch 'main' into feat/1261-timepicker
adamhaeger fb4b812
minor fix
adamhaeger c418402
fixing sonarcube complaoints
adamhaeger 8d79046
fixing sonarcube complaoints
adamhaeger e3ce01b
removed timestamp support
adamhaeger ec5d790
Merge branch 'main' into feat/1261-timepicker
adamhaeger b54a913
time validation cleanup
adamhaeger 4a67ee6
fixes after code rabbit review
adamhaeger 1e691bf
fixed tests
adamhaeger 9722ecb
minor improvement
adamhaeger 19e8034
clean
adamhaeger 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 |
---|---|---|
@@ -0,0 +1,155 @@ | ||
# TimePicker Component | ||
|
||
A React component for time input with intelligent Chrome-like segment typing behavior. | ||
|
||
## Overview | ||
|
||
The TimePicker component provides an intuitive time input interface with separate segments for hours, minutes, seconds (optional), and AM/PM period (for 12-hour format). It features smart typing behavior that mimics Chrome's date/time input controls. | ||
|
||
## Features | ||
|
||
### Smart Typing Behavior | ||
|
||
- **Auto-coercion**: Invalid entries are automatically corrected (e.g., typing "9" in hours becomes "09") | ||
- **Progressive completion**: Type digits sequentially to build complete values (e.g., "1" → "01", then "5" → "15") | ||
- **Buffer management**: Handles rapid typing with timeout-based commits to prevent race conditions | ||
- **Auto-advance**: Automatically moves to next segment when current segment is complete | ||
|
||
### Keyboard Navigation | ||
|
||
- **Arrow keys**: Navigate between segments and increment/decrement values | ||
- **Tab**: Standard tab navigation between segments | ||
- **Delete/Backspace**: Clear current segment | ||
- **Separators**: Type ":", ".", "," or space to advance to next segment | ||
|
||
### Format Support | ||
|
||
- **24-hour format**: "HH:mm" or "HH:mm:ss" | ||
- **12-hour format**: "HH:mm a" or "HH:mm:ss a" (with AM/PM) | ||
- **Flexible display**: Configurable time format with optional seconds | ||
|
||
## Usage | ||
|
||
```tsx | ||
import { TimePicker } from 'src/app-components/TimePicker/TimePicker'; | ||
|
||
// Basic usage | ||
<TimePicker | ||
id="time-input" | ||
value="14:30" | ||
onChange={(value) => console.log(value)} | ||
aria-label="Select time" | ||
/> | ||
|
||
// With 12-hour format and seconds | ||
<TimePicker | ||
id="time-input" | ||
value="2:30:45 PM" | ||
format="HH:mm:ss a" | ||
onChange={(value) => console.log(value)} | ||
aria-label="Select appointment time" | ||
/> | ||
adamhaeger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
## Props | ||
|
||
### Required Props | ||
|
||
- `id: string` - Unique identifier for the component | ||
- `onChange: (value: string) => void` - Callback when time value changes | ||
- `aria-label: string` - Accessibility label for the time picker | ||
|
||
### Optional Props | ||
|
||
- `value?: string` - Current time value in the specified format | ||
- `format?: TimeFormat` - Time format string (default: "HH:mm") | ||
- `disabled?: boolean` - Whether the component is disabled | ||
- `readOnly?: boolean` - Whether the component is read-only | ||
- `className?: string` - Additional CSS classes | ||
- `placeholder?: string` - Placeholder text when empty | ||
|
||
## Component Architecture | ||
|
||
### Core Components | ||
|
||
#### TimePicker (Main Component) | ||
|
||
- Manages overall time state and validation | ||
- Handles format parsing and time value composition | ||
- Coordinates segment navigation and focus management | ||
|
||
#### TimeSegment | ||
|
||
- Individual input segment for hours, minutes, seconds, or period | ||
- Implements Chrome-like typing behavior with buffer management | ||
- Handles keyboard navigation and value coercion | ||
|
||
### Supporting Modules | ||
|
||
#### segmentTyping.ts | ||
|
||
- **Input Processing**: Smart coercion logic for different segment types | ||
- **Buffer Management**: Handles multi-character input with timeouts | ||
- **Validation**: Ensures values stay within valid ranges | ||
|
||
#### keyboardNavigation.ts | ||
|
||
- **Navigation Logic**: Arrow key navigation between segments | ||
- **Value Manipulation**: Increment/decrement with arrow keys | ||
- **Key Handling**: Special key processing (Tab, Delete, etc.) | ||
|
||
#### timeFormatUtils.ts | ||
|
||
- **Format Parsing**: Converts format strings to display patterns | ||
- **Value Formatting**: Formats time values for display | ||
- **Validation**: Validates time format strings | ||
|
||
## Typing Behavior Details | ||
|
||
### Hour Input | ||
|
||
- **24-hour mode**: First digit 0-2 waits for second digit, 3-9 auto-coerces to 0X | ||
- **12-hour mode**: First digit 0-1 waits for second digit, 2-9 auto-coerces to 0X | ||
- **Second digit**: Validates against first digit (e.g., 2X limited to 20-23 in 24-hour) | ||
|
||
### Minute/Second Input | ||
|
||
- **First digit**: 0-5 waits for second digit, 6-9 auto-coerces to 0X | ||
- **Second digit**: Always accepts 0-9 | ||
- **Overflow handling**: Values > 59 are corrected during validation | ||
|
||
### Period Input (AM/PM) | ||
|
||
- **A/a key**: Sets to AM | ||
- **P/p key**: Sets to PM | ||
- **Case insensitive**: Accepts both upper and lower case | ||
|
||
## Buffer Management | ||
|
||
The component uses a sophisticated buffer system to handle rapid typing: | ||
|
||
1. **Immediate Display**: Shows formatted value immediately as user types | ||
2. **Timeout Commit**: Commits buffered value after 1 second of inactivity | ||
3. **Race Condition Prevention**: Uses refs to avoid stale closure issues | ||
4. **State Synchronization**: Keeps buffer state in sync with React state | ||
|
||
## Accessibility | ||
|
||
- **ARIA Labels**: Each segment has descriptive aria-label | ||
- **Keyboard Navigation**: Full keyboard support for all interactions | ||
- **Focus Management**: Proper focus handling and visual indicators | ||
- **Screen Reader Support**: Announces current values and changes | ||
|
||
## Testing | ||
|
||
The component includes comprehensive tests covering: | ||
|
||
- **Typing Scenarios**: Various input patterns and edge cases | ||
- **Navigation**: Keyboard navigation between segments | ||
- **Buffer Management**: Race condition prevention and timeout handling | ||
- **Format Support**: Different time formats and validation | ||
- **Accessibility**: Screen reader compatibility and ARIA support | ||
|
||
## Browser Compatibility | ||
|
||
Designed to work consistently across modern browsers with Chrome-like behavior as the reference implementation. |
163 changes: 163 additions & 0 deletions
163
src/app-components/TimePicker/components/TimePicker.module.css
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,163 @@ | ||
.calendarInputWrapper { | ||
display: flex; | ||
align-items: center; | ||
border-radius: 4px; | ||
border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-strong); | ||
gap: var(--ds-size-1); | ||
background: white; | ||
padding: 2px; | ||
} | ||
|
||
.calendarInputWrapper button { | ||
margin: 1px; | ||
} | ||
|
||
.calendarInputWrapper:hover { | ||
box-shadow: inset 0 0 0 1px var(--ds-color-accent-border-strong); | ||
} | ||
|
||
.segmentContainer { | ||
display: flex; | ||
align-items: center; | ||
flex: 1; | ||
padding: 0 4px; | ||
} | ||
|
||
.segmentContainer input { | ||
border: none; | ||
background: transparent; | ||
padding: 4px 2px; | ||
text-align: center; | ||
font-family: inherit; | ||
font-size: inherit; | ||
} | ||
|
||
.segmentContainer input:focus-visible { | ||
outline: 2px solid var(--ds-color-accent-border-strong); | ||
outline-offset: -1px; | ||
border-radius: 2px; | ||
} | ||
|
||
.segmentSeparator { | ||
color: var(--ds-color-neutral-text-subtle); | ||
user-select: none; | ||
padding: 0 2px; | ||
} | ||
|
||
.timePickerWrapper { | ||
position: relative; | ||
display: inline-block; | ||
} | ||
|
||
.timePickerDropdown { | ||
/*min-width: 320px;*/ | ||
max-width: 400px; | ||
padding: 12px; | ||
box-sizing: border-box; | ||
} | ||
|
||
.dropdownColumns { | ||
display: flex; | ||
gap: 8px; | ||
width: 100%; | ||
box-sizing: border-box; | ||
} | ||
|
||
.dropdownColumn { | ||
flex: 1; | ||
min-width: 60px; | ||
max-width: 80px; | ||
overflow: hidden; | ||
} | ||
|
||
.dropdownLabel { | ||
display: block; | ||
font-size: 0.875rem; | ||
font-weight: 500; | ||
color: var(--ds-color-neutral-text-default); | ||
margin-bottom: 4px; | ||
} | ||
|
||
.dropdownTrigger { | ||
width: 100%; | ||
min-width: 60px; | ||
} | ||
|
||
.dropdownList { | ||
max-height: 160px; | ||
overflow-y: auto; | ||
overflow-x: hidden; | ||
border: 1px solid var(--ds-color-neutral-border-subtle); | ||
border-radius: var(--ds-border-radius-md); | ||
padding: 2px 0; | ||
box-sizing: border-box; | ||
width: 100%; | ||
} | ||
|
||
.dropdownOption { | ||
width: 100%; | ||
padding: 6px 10px; | ||
border: none; | ||
background: transparent; | ||
font-size: 0.875rem; | ||
font-family: inherit; | ||
text-align: center; | ||
cursor: pointer; | ||
color: var(--ds-color-neutral-text-default); | ||
transition: background-color 0.15s ease; | ||
} | ||
|
||
.dropdownOption:hover { | ||
background-color: var(--ds-color-accent-surface-hover); | ||
} | ||
|
||
.dropdownOptionSelected { | ||
background-color: var(--ds-color-accent-base-active) !important; | ||
color: white; | ||
font-weight: 500; | ||
} | ||
|
||
.dropdownOptionSelected:hover { | ||
background-color: var(--ds-color-accent-base-active) !important; | ||
} | ||
|
||
.dropdownOptionFocused { | ||
outline: 2px solid var(--ds-color-accent-border-strong); | ||
outline-offset: -2px; | ||
background-color: var(--ds-color-accent-surface-hover); | ||
} | ||
|
||
.dropdownOptionFocused.dropdownOptionSelected { | ||
/* When option is both focused and selected, prioritize selection styling but add focus outline */ | ||
outline: 2px solid var(--ds-color-neutral-text-on-inverted); | ||
outline-offset: -2px; | ||
} | ||
|
||
.dropdownOptionDisabled { | ||
opacity: 0.5; | ||
cursor: not-allowed; | ||
color: var(--ds-color-neutral-text-subtle); | ||
} | ||
|
||
.dropdownOptionDisabled:hover { | ||
background-color: transparent; | ||
} | ||
|
||
/* Scrollbar styling for dropdown lists */ | ||
.dropdownList::-webkit-scrollbar { | ||
width: 4px; | ||
} | ||
|
||
.dropdownList::-webkit-scrollbar-track { | ||
background: var(--ds-color-neutral-background-subtle); | ||
border-radius: 2px; | ||
} | ||
|
||
.dropdownList::-webkit-scrollbar-thumb { | ||
background: var(--ds-color-neutral-border-default); | ||
border-radius: 2px; | ||
} | ||
|
||
.dropdownList::-webkit-scrollbar-thumb:hover { | ||
background: var(--ds-color-neutral-border-strong); | ||
} |
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.
Uh oh!
There was an error while loading. Please reload this page.