Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
89b8f96
started making timepicker
adamhaeger Aug 14, 2025
c6328f1
timepicker working
adamhaeger Aug 14, 2025
e0023e2
progress
adamhaeger Aug 14, 2025
fc8c3ec
styling updates
adamhaeger Aug 14, 2025
19a7af9
keep picker open
adamhaeger Aug 14, 2025
2ecfe3b
updated error strings
adamhaeger Aug 14, 2025
ddd5179
changed approach to use segments for easier keyboard control. Added c…
adamhaeger Aug 18, 2025
b994597
Merge branch 'main' into feat/1261-timepicker
adamhaeger Aug 19, 2025
bb03ce4
bug fix
adamhaeger Aug 19, 2025
14de8b7
scrolling into view
adamhaeger Aug 20, 2025
be04c40
keyboard navigation working
adamhaeger Aug 22, 2025
6a1d912
fixed input parsing issue, added tests
adamhaeger Aug 22, 2025
23a476b
refactor
adamhaeger Aug 22, 2025
3fdedfa
fix
adamhaeger Aug 22, 2025
8ec76f2
Fixed feedback from PR code review
adamhaeger Aug 25, 2025
ae7e785
Updated tests
adamhaeger Aug 25, 2025
ecceadb
cleanup
adamhaeger Aug 25, 2025
32d8585
cleanup
adamhaeger Aug 26, 2025
e4edc27
added year to displaydata
adamhaeger Aug 27, 2025
e391bf3
Merge branch 'main' into feat/1261-timepicker
adamhaeger Aug 27, 2025
4af9316
fixed test
adamhaeger Aug 27, 2025
8939c81
Merge branch 'main' into feat/1261-timepicker
adamhaeger Sep 3, 2025
8664f7a
wip
adamhaeger Sep 5, 2025
392d284
Merge branch 'main' into feat/1261-timepicker
adamhaeger Sep 5, 2025
086d405
refacor wip
adamhaeger Sep 5, 2025
c86cb88
refactor wip
adamhaeger Sep 8, 2025
d728a51
refactoring and ui improvements
adamhaeger Sep 8, 2025
c364e28
wip
adamhaeger Sep 11, 2025
b06caa3
wip
adamhaeger Sep 11, 2025
ded753d
wip
adamhaeger Sep 11, 2025
c6b0372
cleaned up folder
adamhaeger Sep 11, 2025
c012838
Merge branch 'main' into feat/1261-timepicker
adamhaeger Sep 17, 2025
ed33d3f
test fix
adamhaeger Sep 18, 2025
05a0b5b
cleanup
adamhaeger Sep 18, 2025
bb99f85
cleanup
adamhaeger Sep 18, 2025
8a545cf
more cleanup
adamhaeger Sep 18, 2025
4fc91c9
clean and refactor
adamhaeger Sep 18, 2025
c3c9993
cleanup
adamhaeger Sep 18, 2025
9ce3607
reduced use of useCallback
adamhaeger Sep 18, 2025
a5729ec
refactors
adamhaeger Sep 22, 2025
d504073
Merge branch 'main' into feat/1261-timepicker
adamhaeger Sep 22, 2025
fb4b812
minor fix
adamhaeger Sep 22, 2025
c418402
fixing sonarcube complaoints
adamhaeger Sep 22, 2025
8d79046
fixing sonarcube complaoints
adamhaeger Sep 22, 2025
e3ce01b
removed timestamp support
adamhaeger Sep 22, 2025
ec5d790
Merge branch 'main' into feat/1261-timepicker
adamhaeger Sep 22, 2025
b54a913
time validation cleanup
adamhaeger Sep 22, 2025
4a67ee6
fixes after code rabbit review
adamhaeger Sep 22, 2025
1e691bf
fixed tests
adamhaeger Sep 22, 2025
9722ecb
minor improvement
adamhaeger Sep 22, 2025
19e8034
clean
adamhaeger Sep 22, 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
155 changes: 155 additions & 0 deletions src/app-components/TimePicker/README.md
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"
/>
```

## 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 src/app-components/TimePicker/components/TimePicker.module.css
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);
}
Loading
Loading