-
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
Feat/1261 timepicker #3612
Conversation
/publish |
PR release:
|
📝 WalkthroughWalkthroughAdds a complete TimePicker feature: new app-component, TimeSegment inputs and hooks, many utility modules and types, layout integration (component, config, summary, validation), localization entries, extensive unit/responsive tests, and two console.log debug statements in DateComponent. All additions; no breaking exported-signature changes reported. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 22
♻️ Duplicate comments (1)
src/app-components/TimePicker/TimePicker.tsx (1)
259-270
: Fix the useless assignment to nextIndex.The initial value of
nextIndex
is immediately overwritten and never used.Apply this diff to fix the issue:
- const handleSegmentNavigate = (direction: 'left' | 'right', currentIndex: number) => { - let nextIndex = currentIndex; - - if (direction === 'right') { - nextIndex = (currentIndex + 1) % segments.length; - } else { - nextIndex = (currentIndex - 1 + segments.length) % segments.length; - } + const handleSegmentNavigate = (direction: 'left' | 'right', currentIndex: number) => { + const nextIndex = direction === 'right' + ? (currentIndex + 1) % segments.length + : (currentIndex - 1 + segments.length) % segments.length;
🧹 Nitpick comments (44)
src/language/texts/en.ts (1)
43-43
: Minor copy tweak for consistency with date picker messageTo match the existing “date_picker.invalid_date_message” phrasing and tone, consider adding “the”.
Apply this diff:
- 'time_picker.invalid_time_message': 'Invalid time format. Use format {0}.', + 'time_picker.invalid_time_message': 'Invalid time format. Use the format {0}.',src/language/texts/nn.ts (1)
45-47
: Nynorsk wording – align “tillaten/tillat” with existing date messagesThe date-picker keys in this file use “…dato tillat”, while the new time-picker keys use “…tillaten tid”. For intra-file consistency, consider using the same adjective form as the date messages.
Proposed diff (please have a native reviewer confirm):
- 'time_picker.min_time_exceeded': 'Tida du har vald er før tidlegaste tillaten tid ({0}).', - 'time_picker.max_time_exceeded': 'Tida du har vald er etter seinaste tillaten tid ({0}).', + 'time_picker.min_time_exceeded': 'Tida du har vald er før tidlegaste tid tillat ({0}).', + 'time_picker.max_time_exceeded': 'Tida du har vald er etter seinaste tid tillat ({0}).',If the project prefers “tillaten” here for grammatical reasons, feel free to keep as-is; the main point is to be consistent within the file.
src/app-components/TimePicker/debug.test.tsx (2)
3-3
: Prefer renderWithProviders and user-event; avoid brittle selectors
- Tests should use renderWithProviders to align with our testing setup and future-proof context needs.
- Replace fireEvent.keyPress with userEvent.type for more realistic typing.
- Avoid querySelector with exact aria-label strings; instead, prefer Testing Library queries by role/accessible name.
Example refactor:
-import { fireEvent, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; +import { renderWithProviders } from 'src/test/renderWithProviders'; @@ - const { container } = render( + renderWithProviders( <TimePicker id='test-timepicker' value='' onChange={onChange} aria-label='Select time' />, ); @@ - const hoursInput = container.querySelector('input[aria-label="Select time hours"]') as HTMLInputElement; + const hoursInput = screen.getByRole('textbox', { name: /hours/i }); @@ - fireEvent.keyPress(hoursInput, { key: '2', charCode: 50 }); + await userEvent.type(hoursInput, '2'); @@ - fireEvent.keyPress(hoursInput, { key: '2', charCode: 50 }); + await userEvent.type(hoursInput, '2'); @@ - expect(hoursInput.value).toBe('22'); + expect(hoursInput).toHaveValue('22');Note: If TimePicker relies on timers internally, keep the fake timers; user-event can still work with them.
Also applies to: 19-26, 28-29, 42-44, 53-55, 61-61
7-16
: Consider skipping or converting this debug-spec into an actionable unit testThis file reads like an investigation aid (naming and extensive logging). If it’s not intended as a stable test, either mark it as skipped or transform it into an assertion-driven spec that validates expected behavior across formats/locales.
Option A (skip):
-describe('Debug typing behavior', () => { +describe.skip('Debug typing behavior', () => {Option B: Keep and harden (apply the refactor above and add concrete assertions on onChange calls).
I can help convert this into a deterministic typing-behavior spec exercising edge cases (e.g., 0-prefixed hours, overflow to minutes, AM/PM boundaries).
Also applies to: 17-63
src/layout/TimePicker/TimePickerComponent.test.tsx (1)
68-84
: Seconds visibility test is fine, but consider asserting accessible names tooAsserting count is good; adding accessible-name checks makes it stricter without being brittle.
Example:
- const inputs = screen.getAllByRole('textbox'); - expect(inputs).toHaveLength(3); // Hours, minutes, and seconds + const inputs = screen.getAllByRole('textbox'); + expect(inputs).toHaveLength(3); // Hours, minutes, and seconds + expect(inputs[2]).toHaveAccessibleName(/seconds\b/i);src/layout/TimePicker/useTimePickerValidation.ts (3)
123-129
: Add bindingKey to component validations for consistencyAttach the binding key so validations map cleanly back to the component binding in summary/UX.
Apply this diff:
validations.push({ message: { key: 'time_picker.invalid_time_message', params: [format] }, severity: 'error', source: FrontendValidationSource.Component, category: ValidationMask.Component, + bindingKey: 'simpleBinding', }); @@ validations.push({ message: { key: 'time_picker.min_time_exceeded', params: [minTime] }, severity: 'error', source: FrontendValidationSource.Component, category: ValidationMask.Component, + bindingKey: 'simpleBinding', }); @@ validations.push({ message: { key: 'time_picker.max_time_exceeded', params: [maxTime] }, severity: 'error', source: FrontendValidationSource.Component, category: ValidationMask.Component, + bindingKey: 'simpleBinding', });Also applies to: 136-141, 148-153
9-67
: Avoid duplicating time parsing logic; consider a strict validator in shared utilsThis file reimplements a strict parser while timeConstraintUtils.parseTimeString is permissive by design. To avoid drift:
- Introduce a “strict” parser (e.g., validateTimeString or parseTimeStringStrict) in src/app-components/TimePicker/timeConstraintUtils.ts and reuse it here, or
- Centralize the regex into a shared util to keep TimePicker, Summary, and validation in sync.
I can draft a minimal strict parse helper in timeConstraintUtils and update the hook accordingly if you want.
71-105
: Duplicate ISO-to-display conversion; centralize for reuseextractTimeFromValue duplicates the display logic that TimePicker.useDisplayData already has. Consider moving the conversion to a shared utility (e.g., timeFormatUtils) and consume it both here and in the component/summary to avoid divergence.
Would you like me to propose a small helper in timeFormatUtils and replace both sites?
src/app-components/TimePicker/timeConstraintUtils.test.ts (2)
8-25
: Remove duplicated type declarations; import from the module to fix lint warningLocal interfaces duplicate exported types and trigger an unused var warning for TimeConstraints. Import the types instead.
Apply this diff:
import { getNextValidValue, getSegmentConstraints, isTimeInRange, parseTimeString, } from 'src/app-components/TimePicker/timeConstraintUtils'; -interface TimeValue { - hours: number; - minutes: number; - seconds: number; - period: 'AM' | 'PM'; -} - -interface TimeConstraints { - minTime?: string; - maxTime?: string; -} - -interface SegmentConstraints { - min: number; - max: number; - validValues: number[]; -} +import type { TimeValue, SegmentConstraints } from 'src/app-components/TimePicker/timeConstraintUtils';
116-120
: Strengthen edge-case coverage: seconds and immutability
- isTimeInRange: add seconds-sensitive assertions.
- getSegmentConstraints: add 'seconds' segment boundary test.
- getNextValidValue: ensure validValues is not mutated (reverse() in impl currently mutates).
Apply this diff to append tests:
@@ describe('isTimeInRange', () => { @@ it('should return true when no constraints provided', () => { const result = isTimeInRange(sampleTime, {}, 'HH:mm'); expect(result).toBe(true); }); + it('should respect seconds when format includes seconds (before min seconds)', () => { + const t: TimeValue = { hours: 14, minutes: 30, seconds: 1, period: 'PM' }; + const constraints = { minTime: '14:30:02', maxTime: '14:31:00' }; + expect(isTimeInRange(t, constraints, 'HH:mm:ss')).toBe(false); + }); + it('should respect seconds when at exact boundary', () => { + const t: TimeValue = { hours: 14, minutes: 30, seconds: 2, period: 'PM' }; + const constraints = { minTime: '14:30:02', maxTime: '14:31:00' }; + expect(isTimeInRange(t, constraints, 'HH:mm:ss')).toBe(true); + }); }); @@ describe('getSegmentConstraints', () => { @@ expect(result.validValues).toEqual(Array.from({ length: 60 }, (_, i) => i)); }); + it('should constrain seconds when at min boundary', () => { + const currentTime: TimeValue = { hours: 14, minutes: 30, seconds: 0, period: 'PM' }; + const constraints = { minTime: '14:30:15' }; + const result = getSegmentConstraints('seconds', currentTime, constraints, 'HH:mm:ss'); + expect(result.min).toBe(15); + expect(result.max).toBe(59); + expect(result.validValues[0]).toBe(15); + }); }); @@ describe('getNextValidValue', () => { @@ it('should skip invalid values and find next valid one', () => { const constraints: SegmentConstraints = { min: 5, max: 20, validValues: [5, 8, 12, 15, 20], }; const result = getNextValidValue(5, 'up', constraints); expect(result).toBe(8); }); + it('should not mutate constraints.validValues (order preserved after call)', () => { + const constraints: SegmentConstraints = { + min: 0, + max: 10, + validValues: [0, 3, 7, 10], + }; + const copy = [...constraints.validValues]; + getNextValidValue(5, 'down', constraints); + expect(constraints.validValues).toEqual(copy); + }); });If this last test fails, we should adjust getNextValidValue to avoid mutating arrays (use a shallow copy before reverse()).
Also applies to: 151-166, 209-218
src/app-components/TimePicker/segmentTyping.test.ts (1)
224-243
: Add 12-hour advancement and minute-buffer edge assertionsExpand coverage to ensure 12h auto-advance and minute single-digit completion behave as intended.
Apply this diff to append tests:
@@ describe('shouldAdvanceSegment', () => { @@ it('should not advance from seconds segment', () => { expect(shouldAdvanceSegment('seconds', '59', false)).toBe(false); }); + it('should advance on single-digit 2..9 in 12h mode', () => { + expect(shouldAdvanceSegment('hours', '2', true)).toBe(true); + expect(shouldAdvanceSegment('hours', '1', true)).toBe(false); + }); }); + + describe('processSegmentBuffer - minutes single digit completion', () => { + it('should mark minutes single-digit >5 as complete', () => { + expect(processSegmentBuffer('7', 'minutes', false)).toEqual({ + displayValue: '07', + actualValue: 7, + isComplete: true, + }); + }); + });src/app-components/TimePicker/typingBehavior.test.tsx (1)
233-252
: Avoid stale node after rerender: re-query DOMAfter rerender, reuse of the pre-rerender input reference is brittle. Re-query to ensure you hold the current element.
Apply this diff:
- // Type another "2" - should result in "22", not "02" - fireEvent.keyPress(hoursInput, { key: '2', charCode: 50 }); - expect(hoursInput.value).toBe('22'); + // Re-query after rerender to avoid stale reference + const hoursInputAfter = container.querySelector('input[aria-label="Select time hours"]') as HTMLInputElement; + fireEvent.keyPress(hoursInputAfter, { key: '2', charCode: 50 }); + expect(hoursInputAfter.value).toBe('22');src/app-components/TimePicker/keyboardNavigation.test.ts (3)
15-21
: Remove unused local type to satisfy lint and avoid drift.
SegmentNavigationResult
is defined but never used (see static analysis warning). Drop it to keep the test lean and quiet.- interface SegmentNavigationResult { - shouldNavigate: boolean; - direction?: 'left' | 'right'; - shouldIncrement?: boolean; - shouldDecrement?: boolean; - preventDefault: boolean; - }
1-6
: Prefer importing shared types and strongly typing the mock event.
- Reuse the exported
SegmentType
from the implementation to prevent type drift.- Define a local
KeyEvent
from the function signature instead of casting viaunknown
.import { getNextSegmentIndex, handleSegmentKeyDown, handleValueDecrement, handleValueIncrement, + type SegmentType, } from 'src/app-components/TimePicker/keyboardNavigation'; -interface MockKeyboardEvent { - key: string; - preventDefault: () => void; -} - -type SegmentType = 'hours' | 'minutes' | 'seconds' | 'period'; +type KeyEvent = Parameters<typeof handleSegmentKeyDown>[0];Follow-up: Replace casts like
as unknown as MockKeyboardEvent
withas KeyEvent
at their call sites.Also applies to: 8-14
34-40
: Optional: assert preventDefault for all arrow cases.You already assert it for ArrowUp. Mirroring that on Down/Left/Right will guard regressions in keyboard behavior.
Also applies to: 42-58
src/app-components/TimePicker/timeFormatUtils.test.ts (1)
8-13
: ImportTimeValue
from source to keep types centralized.Prevents duplicate definitions and reduces the risk of divergence if the shape changes.
-import interface TimeValue { - hours: number; - minutes: number; - seconds: number; - period: 'AM' | 'PM'; -} +import type { TimeValue } from 'src/app-components/TimePicker/timeConstraintUtils';src/app-components/TimePicker/TimeSegment.test.tsx (3)
84-99
: Use keys instead ofclear()
to enter a single-digit hour; blur to ensure commit.Makes the test align with the component’s event model and removes flakiness from timeout-based commits.
- await userEvent.clear(input); - await userEvent.type(input, '8'); - - expect(onValueChange).toHaveBeenCalledWith(8); + await userEvent.click(input); + await userEvent.keyboard('{Backspace}'); + await userEvent.type(input, '8'); + await userEvent.tab(); + expect(onValueChange).toHaveBeenCalledWith(8);
100-114
: Same rationale for two-digit entry.Use Backspace and blur to commit deterministically.
- await userEvent.clear(input); - await userEvent.type(input, '11'); - - expect(onValueChange).toHaveBeenCalledWith(11); + await userEvent.click(input); + await userEvent.keyboard('{Backspace}'); + await userEvent.type(input, '11'); + await userEvent.tab(); + expect(onValueChange).toHaveBeenCalledWith(11);
220-252
: Optional: make the period toggle test self-contained withrerender
to avoid multiple textboxes.Using
screen.getAllByRole('textbox')[1]
is a bit brittle as other tests evolve. Arerender
keeps the test scoped to a single input.- jest.clearAllMocks(); - - // Simulate component with PM value for ArrowDown test - render( - <TimeSegment - {...defaultProps} - type='period' - value='PM' - onValueChange={onValueChange} - />, - ); - const pmInput = screen.getAllByRole('textbox')[1]; // Get the second input (PM one) + jest.clearAllMocks(); + const { rerender } = render( + <TimeSegment + {...defaultProps} + type='period' + value='PM' + onValueChange={onValueChange} + />, + ); + const pmInput = screen.getByRole('textbox');src/layout/TimePicker/config.ts (1)
41-43
: Clarify seconds/12-hour expectations inminTime
/maxTime
descriptions.Given
format
can include seconds or AM/PM, the current “HH:mm” wording may confuse users. Either enforce HH:mm in validation or broaden the description to note accepted forms.- .setDescription('Sets the earliest allowed time in HH:mm format.') + .setDescription('Sets the earliest allowed time. Use HH:mm (or HH:mm:ss when seconds are enabled).') ... - .setDescription('Sets the latest allowed time in HH:mm format.') + .setDescription('Sets the latest allowed time. Use HH:mm (or HH:mm:ss when seconds are enabled).')Also applies to: 51-53
src/layout/TimePicker/TimePickerComponent.tsx (1)
29-51
: Consider timezone intent when persisting ISO timestamps.The
timeStamp
branch storesnow
with local time components as an ISO string (UTC). When round-tripping, you convert back using localDate
, which is consistent, but the stored value will vary by client timezone. If the backend expects “local wall time with date” rather than an absolute instant, consider storing a local-date-time string (e.g.,YYYY-MM-DDTHH:mm[:ss]
withoutZ
) or also persisting timezone context.Would you like a small helper to parse/format “local date-time” strings without timezone conversion?
src/app-components/TimePicker/TimePicker.tsx (2)
69-79
: Consider using a more robust mobile detection library.While the current implementation works, user agent string detection can be unreliable. Consider using a dedicated library like
react-device-detect
for more accurate device detection.Would you like me to provide an implementation using a more robust device detection library?
145-191
: Consider extracting the scroll centering logic to a utility function.The scroll centering logic is repeated three times (hours, minutes, seconds). This could be extracted to reduce duplication.
Apply this diff to extract the repeated logic:
+ const scrollToSelectedOption = (containerRef: React.RefObject<HTMLDivElement | null>, selector: string) => { + if (containerRef.current) { + const selectedOption = containerRef.current.querySelector(selector); + if (selectedOption) { + const container = containerRef.current; + const elementTop = (selectedOption as HTMLElement).offsetTop; + const elementHeight = (selectedOption as HTMLElement).offsetHeight; + const containerHeight = container.offsetHeight; + + // Center the selected item in the container + container.scrollTop = elementTop - containerHeight / 2 + elementHeight / 2; + } + } + }; useEffect(() => { if (showDropdown) { // Small delay to ensure DOM is rendered setTimeout(() => { - // Scroll hours into view - if (hoursListRef.current) { - const selectedHour = hoursListRef.current.querySelector(`.${styles.dropdownOptionSelected}`); - if (selectedHour) { - const container = hoursListRef.current; - const elementTop = (selectedHour as HTMLElement).offsetTop; - const elementHeight = (selectedHour as HTMLElement).offsetHeight; - const containerHeight = container.offsetHeight; - - // Center the selected item in the container - container.scrollTop = elementTop - containerHeight / 2 + elementHeight / 2; - } - } - - // Scroll minutes into view - if (minutesListRef.current) { - const selectedMinute = minutesListRef.current.querySelector(`.${styles.dropdownOptionSelected}`); - if (selectedMinute) { - const container = minutesListRef.current; - const elementTop = (selectedMinute as HTMLElement).offsetTop; - const elementHeight = (selectedMinute as HTMLElement).offsetHeight; - const containerHeight = container.offsetHeight; - - container.scrollTop = elementTop - containerHeight / 2 + elementHeight / 2; - } - } - - // Scroll seconds into view - if (secondsListRef.current) { - const selectedSecond = secondsListRef.current.querySelector(`.${styles.dropdownOptionSelected}`); - if (selectedSecond) { - const container = secondsListRef.current; - const elementTop = (selectedSecond as HTMLElement).offsetTop; - const elementHeight = (selectedSecond as HTMLElement).offsetHeight; - const containerHeight = container.offsetHeight; - - container.scrollTop = elementTop - containerHeight / 2 + elementHeight / 2; - } - } + scrollToSelectedOption(hoursListRef, `.${styles.dropdownOptionSelected}`); + scrollToSelectedOption(minutesListRef, `.${styles.dropdownOptionSelected}`); + scrollToSelectedOption(secondsListRef, `.${styles.dropdownOptionSelected}`); }, 0); } }, [showDropdown]);src/app-components/TimePicker/timeFormatUtils.ts (1)
51-75
: Consider removing the unused _format parameter.The
_format
parameter inparseSegmentInput
is prefixed with underscore but never used. If it's intended for future use, consider adding a comment. Otherwise, remove it.Apply this diff if the parameter is not needed:
export const parseSegmentInput = ( input: string, segmentType: SegmentType, - _format: TimeFormat, ): number | string | null => {
If it's intended for future use, add a comment:
export const parseSegmentInput = ( input: string, segmentType: SegmentType, + // eslint-disable-next-line @typescript-eslint/no-unused-vars _format: TimeFormat, ): number | string | null => {
src/app-components/TimePicker/dropdownKeyboardNavigation.test.tsx (1)
21-32
: Consider adding error handling for async operations.The
openDropdown
helper should handle potential errors when the dropdown fails to open.Apply this diff to add error handling:
const openDropdown = async () => { const triggerButton = screen.getByRole('button', { name: /open time picker/i }); fireEvent.click(triggerButton); - await waitFor(() => { - const dropdown = screen.getByRole('dialog'); - expect(dropdown).toBeInTheDocument(); - expect(dropdown).toHaveAttribute('aria-hidden', 'false'); - }); + await waitFor(() => { + const dropdown = screen.getByRole('dialog'); + expect(dropdown).toBeInTheDocument(); + expect(dropdown).toHaveAttribute('aria-hidden', 'false'); + }, { timeout: 3000 }).catch(() => { + throw new Error('Failed to open dropdown within timeout'); + }); return screen.getByRole('dialog'); };src/app-components/TimePicker/dropdownBehavior.ts (3)
62-76
: Page jump calculation can divide by zero; and “60 minutes worth of options” is minutes-specific
- If
stepMinutes
is 0/invalid,Math.floor(60 / stepMinutes)
breaks.- This utility is only correct for a minutes list; ensure it isn’t used for hours/period lists.
Apply this diff to harden the function:
- const itemsToJump = Math.max(1, Math.floor(60 / stepMinutes)); + const safeStep = Number.isFinite(stepMinutes) && stepMinutes > 0 ? stepMinutes : 1; + const itemsToJump = Math.max(1, Math.floor(60 / safeStep));And add a JSDoc note to limit usage to the minutes column or rename to
getMinutePageJumpIndex
.Confirm this function is only invoked for the minutes column.
125-132
: Scroll position not clamped to end; can overshoot containerCentering is fine, but we should cap to the max scrollable position.
Apply this diff (requires
totalOptions
to compute max; if not available, at least clamp to 0):-export const calculateScrollPosition = (index: number, containerHeight: number, itemHeight: number): number => { +export const calculateScrollPosition = ( + index: number, + containerHeight: number, + itemHeight: number, + totalOptions?: number, +): number => { // Calculate position to center the item const itemTop = index * itemHeight; const scrollTo = itemTop - containerHeight / 2 + itemHeight / 2; - // Don't scroll negative - return Math.max(0, scrollTo); + // Clamp within scrollable range + const min = 0; + const max = + totalOptions && totalOptions > 0 ? Math.max(0, totalOptions * itemHeight - containerHeight) : Number.POSITIVE_INFINITY; + return Math.min(Math.max(min, scrollTo), max); };
91-105
: Case-insensitive match for period stringsIf callers pass 'am'/'pm',
findNearestOptionIndex
will miss and default to index 0. Support case-insensitive string matching.Apply this diff:
- const exactIndex = options.findIndex((opt) => opt.value === value); + const exactIndex = options.findIndex((opt) => + typeof opt.value === 'string' && typeof value === 'string' + ? opt.value.toLowerCase() === value.toLowerCase() + : opt.value === value, + );src/app-components/TimePicker/TimePicker.module.css (4)
7-7
: Use design token instead of hardcoded white backgroundHardcoded
white
can break theming (e.g., dark mode). Prefer a design token background.Apply this diff:
- background: white; + background: var(--ds-color-neutral-background-default);
118-126
: Avoid !important for selected option styling
!important
reduces maintainability and can interfere with focus/hover states. Increase specificity or use stateful attributes (e.g.,[aria-selected="true"]
) instead.Apply this diff:
-.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; -} +.dropdownOptionSelected, +.dropdownOption[aria-selected='true'], +.dropdownOptionSelected:hover, +.dropdownOption[aria-selected='true']:hover { + background-color: var(--ds-color-accent-base-active); + color: white; + font-weight: 500; +}
128-132
: Ensure keyboard focus visible even if JS class is missingRelying only on a class for focus can miss native keyboard focus. Add a
:focus-visible
fallback.Apply this diff:
.dropdownOptionFocused { outline: 2px solid var(--ds-color-accent-border-strong); outline-offset: -2px; background-color: var(--ds-color-accent-surface-hover); } + +.dropdownOption:focus-visible { + outline: 2px solid var(--ds-color-accent-border-strong); + outline-offset: -2px; +}
56-61
: Revisit dropdown min/max widthMin-width is commented out; depending on content length, columns can wrap unpredictably. Consider an explicit min width or responsive rule.
Example:
/* prevents column wrapping for 3–4 columns */ .timePickerDropdown { min-width: 24rem; /* adjust to DS spacing */ }src/layout/TimePicker/index.tsx (2)
47-76
: Display formatting duplicates logic from TimePickerComponent; extract a shared utilityFormatting 12/24h with optional seconds is implemented here and in
TimePickerComponent
(see relevant snippet). Extract to a shared formatter to keep behavior consistent.I can factor this into
timeFormatUtils.formatTimeDisplay({ date, format })
and replace both call sites.
110-125
: Simplify errors extraction and remove unused local
_component
is never used, and destructuringconst [errors] = [validation[0] ?? []];
is unnecessarily indirect.Apply this diff:
- const _component = useLayoutLookups().getComponent(baseComponentId, 'TimePicker'); + // component lookup not needed here; keeping lookups for binding validation only @@ - const [errors] = [validation[0] ?? []]; - - return errors; + return validation[0] ?? [];src/app-components/TimePicker/TimeSegment.tsx (3)
172-218
: onKeyPress is deprecated; prefer onBeforeInput or unify onKeyDownRelying on
onKeyPress
will become brittle across browsers/React versions. Migrate toonBeforeInput
for character input or handle characters inonKeyDown
.High-level approach:
- Replace
onKeyPress
withonBeforeInput={(e) => { const char = e.data; ... }}
.- Keep non-character logic in
onKeyDown
.
20-36
: Propsmin
/max
are unusedThey are declared but not used in the component. Either remove them or enforce them during commit/increment/decrement.
Apply this diff if not needed:
- min: number; - max: number;
295-297
: Redundant conditional for maxLengthBoth branches are
2
.Apply this diff:
- maxLength={type === 'period' ? 2 : 2} + maxLength={2}src/app-components/TimePicker/segmentTyping.ts (7)
208-214
: Robustness: guardparseInt
result before comparison inshouldAdvanceSegment
If
buffer
is ever non-numeric in this path,digit
becomesNaN
and comparisons return falsey subtly. Add an explicit guard for clarity and safety.Apply this diff:
if (buffer.length === 1) { - const digit = parseInt(buffer, 10); + const digit = parseInt(buffer, 10); + if (Number.isNaN(digit)) { + return false; + } if (is12Hour) { return digit >= 2; // 2-9 get coerced and advance } else { return digit >= 3; // 3-9 get coerced and advance }
234-234
: TimeFormat detection should be case-insensitiveUsing
format.includes('a')
missesA
if upstream tokenization ever uses uppercase. Prefer a case-insensitive test.Apply this diff:
- const is12Hour = format.includes('a'); + const is12Hour = /a/i.test(format);
45-54
: Confirm 12-hour semantics for00
→ current behavior sets01
, not12
In 12h mode with first digit 0 and second 0, you coerce to
'01'
. Many time pickers treat00
as12
(12 AM/PM). If your UX spec expects12
, adjust the coercion.If aligning to
12
is desired, apply:- // 01-09 valid, but 00 becomes 01 - finalValue = digitNum === 0 ? '01' : `0${digit}`; + // 01-09 valid; treat 00 as 12 in 12-hour clocks + finalValue = digitNum === 0 ? '12' : `0${digit}`;
72-95
: Minute/second typing never requests auto-advance — confirm UX
processMinuteInput
always returnsshouldAdvance: false
. That’s consistent with your comment (“Chrome behavior”) andshouldAdvanceSegment()
returningfalse
for minutes/seconds. If design ever changes to auto-advance after two digits, you can flip the return to true whencurrentBuffer.length === 1
.
120-146
: Minor: underscore_is12Hour
is unusedThe underscore suggests intentional, but if not planned for future use, consider removing the parameter to avoid confusion.
1-279
: Consolidate segment constraints to avoid duplicationRanges are currently codified in multiple places (
processHourInput
,processMinuteInput
,coerceToValidRange
, andshouldAdvanceSegment
). Centralizing constraints (e.g., via a small config map { hours12: [1,12], hours24: [0,23], mins: [0,59] }) reduces drift and eases future changes.If you want, I can propose a small constants module and refactor these helpers to reference it.
159-167
: Ensure empty hours respect 12h vs 24h formatTo make
commitSegmentValue
default hours correctly in 12-hour mode, add an optionalis12Hour
flag (defaulting tofalse
) and thread it through each call site where you know the display format:• In
src/app-components/TimePicker/segmentTyping.ts
– Change the signature at line 159:-export const commitSegmentValue = ( - value: number | string | null, - segmentType: SegmentType, -): number | string => { +export const commitSegmentValue = ( + value: number | string | null, + segmentType: SegmentType, + is12Hour: boolean = false, +): number | string => {– Update the “empty hours” return to:
if (value === null) { if (segmentType === 'minutes' || segmentType === 'seconds') { return 0; // Fill empty minutes/seconds with 00 } - return 0; // Default for hours too + return is12Hour ? 12 : 0; // Default hours: 12 for 12h, 0 for 24h }• In
src/app-components/TimePicker/TimeSegment.tsx
Locate each invocation ofcommitSegmentValue(buffer.actualValue, type)
and change to pass the 12h flag based on yourformat
prop (e.g.format.includes('a')
):// Inside commitBuffer (≈line 98) - const committedValue = commitSegmentValue(buffer.actualValue, type); + const committedValue = commitSegmentValue(buffer.actualValue, type, format.includes('a')); // In immediate-commit path (≈line 198) - const committedValue = commitSegmentValue(buffer.actualValue, type); + const committedValue = commitSegmentValue(buffer.actualValue, type, format.includes('a'));• Tests in
segmentTyping.test.ts
remain unchanged (the new default is backwards-compatible for minutes/seconds and hours in 24h mode), but consider adding a case to assert that in 12h mode,commitSegmentValue(null, 'hours', true)
returns12
.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (27)
src/app-components/TimePicker/TimePicker.module.css
(1 hunks)src/app-components/TimePicker/TimePicker.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment.test.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment.tsx
(1 hunks)src/app-components/TimePicker/debug.test.tsx
(1 hunks)src/app-components/TimePicker/dropdownBehavior.test.ts
(1 hunks)src/app-components/TimePicker/dropdownBehavior.ts
(1 hunks)src/app-components/TimePicker/dropdownKeyboardNavigation.test.tsx
(1 hunks)src/app-components/TimePicker/keyboardNavigation.test.ts
(1 hunks)src/app-components/TimePicker/keyboardNavigation.ts
(1 hunks)src/app-components/TimePicker/segmentTyping.test.ts
(1 hunks)src/app-components/TimePicker/segmentTyping.ts
(1 hunks)src/app-components/TimePicker/timeConstraintUtils.test.ts
(1 hunks)src/app-components/TimePicker/timeConstraintUtils.ts
(1 hunks)src/app-components/TimePicker/timeFormatUtils.test.ts
(1 hunks)src/app-components/TimePicker/timeFormatUtils.ts
(1 hunks)src/app-components/TimePicker/typingBehavior.test.tsx
(1 hunks)src/language/texts/en.ts
(1 hunks)src/language/texts/nb.ts
(1 hunks)src/language/texts/nn.ts
(1 hunks)src/layout/Date/DateComponent.tsx
(2 hunks)src/layout/TimePicker/TimePickerComponent.test.tsx
(1 hunks)src/layout/TimePicker/TimePickerComponent.tsx
(1 hunks)src/layout/TimePicker/TimePickerSummary.tsx
(1 hunks)src/layout/TimePicker/config.ts
(1 hunks)src/layout/TimePicker/index.tsx
(1 hunks)src/layout/TimePicker/useTimePickerValidation.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/layout/TimePicker/TimePickerSummary.tsx
src/layout/TimePicker/TimePickerComponent.test.tsx
src/app-components/TimePicker/timeConstraintUtils.test.ts
src/app-components/TimePicker/debug.test.tsx
src/language/texts/nb.ts
src/language/texts/en.ts
src/app-components/TimePicker/TimeSegment.test.tsx
src/app-components/TimePicker/dropdownBehavior.ts
src/app-components/TimePicker/segmentTyping.test.ts
src/layout/Date/DateComponent.tsx
src/app-components/TimePicker/dropdownKeyboardNavigation.test.tsx
src/language/texts/nn.ts
src/app-components/TimePicker/timeConstraintUtils.ts
src/app-components/TimePicker/TimeSegment.tsx
src/app-components/TimePicker/typingBehavior.test.tsx
src/app-components/TimePicker/timeFormatUtils.ts
src/app-components/TimePicker/dropdownBehavior.test.ts
src/app-components/TimePicker/timeFormatUtils.test.ts
src/app-components/TimePicker/TimePicker.tsx
src/app-components/TimePicker/segmentTyping.ts
src/layout/TimePicker/useTimePickerValidation.ts
src/app-components/TimePicker/keyboardNavigation.test.ts
src/app-components/TimePicker/keyboardNavigation.ts
src/layout/TimePicker/TimePickerComponent.tsx
src/layout/TimePicker/config.ts
src/layout/TimePicker/index.tsx
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In tests, use
renderWithProviders
fromsrc/test/renderWithProviders.tsx
to supply required form layout context
Files:
src/layout/TimePicker/TimePickerComponent.test.tsx
src/app-components/TimePicker/timeConstraintUtils.test.ts
src/app-components/TimePicker/debug.test.tsx
src/app-components/TimePicker/TimeSegment.test.tsx
src/app-components/TimePicker/segmentTyping.test.ts
src/app-components/TimePicker/dropdownKeyboardNavigation.test.tsx
src/app-components/TimePicker/typingBehavior.test.tsx
src/app-components/TimePicker/dropdownBehavior.test.ts
src/app-components/TimePicker/timeFormatUtils.test.ts
src/app-components/TimePicker/keyboardNavigation.test.ts
**/*.module.css
📄 CodeRabbit inference engine (CLAUDE.md)
Use CSS Modules for component styling and follow existing patterns in
*.module.css
files
Files:
src/app-components/TimePicker/TimePicker.module.css
src/layout/*/{config.ts,Component.tsx,index.tsx,config.generated.ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Layout components must follow the standardized structure:
config.ts
,Component.tsx
,index.tsx
, and include generated types inconfig.generated.ts
Files:
src/layout/TimePicker/config.ts
src/layout/TimePicker/index.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-22T13:53:28.201Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.201Z
Learning: Applies to **/*.test.{ts,tsx} : In tests, use `renderWithProviders` from `src/test/renderWithProviders.tsx` to supply required form layout context
Applied to files:
src/layout/TimePicker/TimePickerComponent.test.tsx
📚 Learning: 2025-08-22T13:53:28.201Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.201Z
Learning: Applies to src/layout/*/{config.ts,Component.tsx,index.tsx,config.generated.ts} : Layout components must follow the standardized structure: `config.ts`, `Component.tsx`, `index.tsx`, and include generated types in `config.generated.ts`
Applied to files:
src/layout/TimePicker/config.ts
🧬 Code graph analysis (19)
src/layout/TimePicker/TimePickerSummary.tsx (9)
src/layout/Summary2/SummaryComponent2/types.ts (1)
Summary2Props
(1-3)src/layout/Summary2/summaryStoreContext.tsx (2)
useSummaryOverrides
(43-69)useSummaryProp
(29-37)src/layout/TimePicker/index.tsx (1)
useDisplayData
(37-80)src/features/validation/selectors/unifiedValidationsForNode.ts (1)
useUnifiedValidationsForNode
(15-27)src/features/validation/utils.ts (1)
validationsOfSeverity
(39-44)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/layout/Summary2/SummaryComponent2/ComponentSummary.tsx (1)
SummaryFlex
(123-151)src/layout/Summary2/CommonSummaryComponents/SingleValueSummary.tsx (1)
SingleValueSummary
(22-82)src/features/language/Lang.tsx (1)
Lang
(15-23)
src/layout/TimePicker/TimePickerComponent.test.tsx (3)
src/test/renderWithProviders.tsx (1)
renderGenericComponentTest
(683-733)src/layout/TimePicker/TimePickerComponent.tsx (1)
TimePickerComponent
(12-117)src/__mocks__/getLayoutSetsMock.ts (1)
defaultDataTypeMock
(3-3)
src/app-components/TimePicker/timeConstraintUtils.test.ts (1)
src/app-components/TimePicker/timeConstraintUtils.ts (7)
TimeValue
(3-8)TimeConstraints
(10-13)SegmentConstraints
(15-19)parseTimeString
(21-56)isTimeInRange
(58-80)getSegmentConstraints
(82-189)getNextValidValue
(191-217)
src/app-components/TimePicker/debug.test.tsx (1)
src/layout/TimePicker/index.tsx (1)
TimePicker
(30-134)
src/app-components/TimePicker/TimeSegment.test.tsx (1)
src/app-components/TimePicker/TimeSegment.tsx (2)
TimeSegmentProps
(20-36)TimeSegment
(38-300)
src/app-components/TimePicker/dropdownBehavior.ts (1)
src/app-components/TimePicker/keyboardNavigation.ts (1)
SegmentType
(4-4)
src/app-components/TimePicker/segmentTyping.test.ts (1)
src/app-components/TimePicker/segmentTyping.ts (9)
processHourInput
(18-67)processMinuteInput
(72-95)processPeriodInput
(100-109)processSegmentBuffer
(120-146)isNavigationKey
(114-115)clearSegment
(151-154)commitSegmentValue
(159-167)coerceToValidRange
(172-198)shouldAdvanceSegment
(203-219)
src/app-components/TimePicker/TimeSegment.tsx (3)
src/app-components/TimePicker/keyboardNavigation.ts (4)
SegmentType
(4-4)handleSegmentKeyDown
(14-58)handleValueIncrement
(74-110)handleValueDecrement
(112-148)src/app-components/TimePicker/timeFormatUtils.ts (1)
formatSegmentValue
(28-49)src/app-components/TimePicker/segmentTyping.ts (4)
processSegmentBuffer
(120-146)commitSegmentValue
(159-167)clearSegment
(151-154)handleSegmentCharacterInput
(224-278)
src/app-components/TimePicker/typingBehavior.test.tsx (1)
src/layout/TimePicker/index.tsx (1)
TimePicker
(30-134)
src/app-components/TimePicker/timeFormatUtils.ts (2)
src/app-components/TimePicker/timeConstraintUtils.ts (1)
TimeValue
(3-8)src/app-components/TimePicker/keyboardNavigation.ts (1)
SegmentType
(4-4)
src/app-components/TimePicker/dropdownBehavior.test.ts (1)
src/app-components/TimePicker/dropdownBehavior.ts (9)
roundToStep
(11-11)getInitialHighlightIndex
(16-46)getNextIndex
(51-57)getPageJumpIndex
(62-76)getHomeIndex
(81-81)getEndIndex
(86-86)findNearestOptionIndex
(91-120)calculateScrollPosition
(125-132)shouldScrollToOption
(137-152)
src/app-components/TimePicker/timeFormatUtils.test.ts (2)
src/app-components/TimePicker/timeConstraintUtils.ts (1)
TimeValue
(3-8)src/app-components/TimePicker/timeFormatUtils.ts (4)
formatTimeValue
(5-26)formatSegmentValue
(28-49)parseSegmentInput
(51-75)isValidSegmentInput
(77-117)
src/app-components/TimePicker/segmentTyping.ts (1)
src/app-components/TimePicker/keyboardNavigation.ts (1)
SegmentType
(4-4)
src/layout/TimePicker/useTimePickerValidation.ts (5)
src/app-components/TimePicker/timeConstraintUtils.ts (1)
parseTimeString
(21-56)src/features/validation/index.ts (1)
ComponentValidation
(151-153)src/utils/layout/hooks.ts (1)
useDataModelBindingsFor
(102-112)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/features/formData/FormDataWrite.tsx (1)
FD
(683-1096)
src/app-components/TimePicker/keyboardNavigation.test.ts (1)
src/app-components/TimePicker/keyboardNavigation.ts (6)
SegmentType
(4-4)SegmentNavigationResult
(6-12)handleSegmentKeyDown
(14-58)getNextSegmentIndex
(60-72)handleValueIncrement
(74-110)handleValueDecrement
(112-148)
src/app-components/TimePicker/keyboardNavigation.ts (1)
src/app-components/TimePicker/timeConstraintUtils.ts (1)
SegmentConstraints
(15-19)
src/layout/TimePicker/TimePickerComponent.tsx (6)
src/layout/index.ts (1)
PropsFromGenericComponent
(28-32)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/features/formData/useDataModelBindings.ts (1)
useDataModelBindings
(42-57)src/utils/layout/useLabel.tsx (1)
useLabel
(13-72)src/layout/ComponentStructureWrapper.tsx (1)
ComponentStructureWrapper
(20-48)src/app-components/Flex/Flex.tsx (1)
Flex
(25-84)
src/layout/TimePicker/config.ts (1)
src/codegen/CG.ts (1)
CG
(25-57)
src/layout/TimePicker/index.tsx (12)
src/layout/index.ts (4)
ValidateComponent
(68-70)ValidationFilter
(86-88)PropsFromGenericComponent
(28-32)ValidationFilterFunction
(80-84)src/layout/TimePicker/TimePickerComponent.tsx (1)
TimePickerComponent
(12-117)src/utils/layout/useNodeItem.ts (1)
useNodeFormDataWhenType
(97-103)src/utils/layout/hooks.ts (1)
useExternalItem
(16-22)src/layout/LayoutComponent.tsx (2)
SummaryRendererProps
(167-172)ExprResolver
(41-53)src/layout/Summary/SummaryItemSimple.tsx (1)
SummaryItemSimple
(14-35)src/layout/Summary2/SummaryComponent2/types.ts (1)
Summary2Props
(1-3)src/layout/TimePicker/useTimePickerValidation.ts (1)
useTimePickerValidation
(107-157)src/layout/layout.ts (1)
IDataModelBindings
(61-64)src/features/datamodel/DataModelsProvider.tsx (1)
DataModels
(382-423)src/features/form/layout/LayoutsContext.tsx (1)
useLayoutLookups
(113-113)src/utils/layout/generator/validation/hooks.ts (1)
validateDataModelBindingsAny
(10-56)
🪛 GitHub Actions: Tests
src/layout/TimePicker/TimePickerComponent.test.tsx
[error] 47-47: Element does not have aria-label='Hours'. Actual aria-label='schmable hours'. Tests expecting specific aria-labels are failing.
[error] 65-65: Unable to find a button with name matching '/AM|PM/i'. The component's accessibility labels do not match test expectations.
src/app-components/TimePicker/TimeSegment.test.tsx
[error] 130-130: Expected element to have value, but got undefined. Possibly the input did not receive the expected value.
[error] 149-149: Expected onChange to have been called with 'PM'. It was not called.
[error] 101-101: Cannot find a single element with role 'button' and name '13'. Multiple matches found. Use getAllByRole instead of getByRole.
[error] 149-149: Expected onValueChange to have been called with 'PM'. It was not called.
[error] 130-130: Expected input to have value, but got undefined. Possibly the input did not receive the expected value.
[error] 149-149: Expected onValueChange to have been called with 'PM'. It was not called.
[error] 101-101: Cannot find a single element with role 'button' and name '13'. Multiple matches found. Use getAllByRole instead of getByRole.
[error] 149-149: Expected onValueChange to have been called with 'PM'. It was not called.
src/app-components/TimePicker/dropdownKeyboardNavigation.test.tsx
[error] 42-42: Cannot find a single element with the role 'button' and name '14'. Multiple matches found. Use getAllByRole instead of getByRole.
[error] 75-75: Cannot find a single element with the role 'button' and name '14'. Multiple matches found. Use getAllByRole instead of getByRole.
[error] 101-101: Cannot find a single element with the role 'button' and name '15'. Multiple matches found. Use getAllByRole instead of getByRole.
[error] 126-126: Cannot find a single element with the role 'button' and name '14'. Multiple matches found. Use getAllByRole instead of getByRole.
[error] 171-171: Cannot find a single element with the role 'button' and name '16'. Multiple matches found. Use getAllByRole instead of getByRole.
[error] 42-42: Cannot select element with getByRole('button', { name: '14' }) because multiple matches found. Use getAllByRole and select the correct element.
[error] 75-75: Cannot select element with getByRole('button', { name: '14' }) because multiple matches found. Use getAllByRole and select the correct element.
[error] 101-101: Cannot select element with getByRole('button', { name: '15' }) because multiple matches found. Use getAllByRole and select the correct element.
[error] 126-126: Cannot select element with getByRole('button', { name: '14' }) because multiple matches found. Use getAllByRole and select the correct element.
🪛 GitHub Check: Type-checks, eslint, unit tests and SonarCloud
src/app-components/TimePicker/timeConstraintUtils.test.ts
[warning] 15-15:
'TimeConstraints' is defined but never used. Allowed unused vars must match /^_/u
src/app-components/TimePicker/debug.test.tsx
[warning] 55-55:
Unexpected console statement. Only these console methods are allowed: warn, error
[warning] 44-44:
Unexpected console statement. Only these console methods are allowed: warn, error
[warning] 37-37:
Unexpected console statement. Only these console methods are allowed: warn, error
[warning] 30-30:
Unexpected console statement. Only these console methods are allowed: warn, error
src/layout/Date/DateComponent.tsx
[warning] 26-26:
Unexpected console statement. Only these console methods are allowed: warn, error
[warning] 43-43:
Unexpected console statement. Only these console methods are allowed: warn, error
src/app-components/TimePicker/keyboardNavigation.test.ts
[warning] 15-15:
'SegmentNavigationResult' is defined but never used. Allowed unused vars must match /^_/u
🪛 GitHub Check: CodeQL
src/app-components/TimePicker/TimePicker.tsx
[warning] 260-260: Useless assignment to local variable
The initial value of nextIndex is unused, since it is always overwritten.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Install
src/app-components/TimePicker/dropdownKeyboardNavigation.test.tsx
Outdated
Show resolved
Hide resolved
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.
Actionable comments posted: 14
♻️ Duplicate comments (1)
src/layout/TimePicker/TimePickerComponent.tsx (1)
110-122
: Resolved: placeholder aria-label removedThe earlier placeholder aria-label has been removed; the control is correctly named via the associated Label.
🧹 Nitpick comments (52)
src/app-components/TimePicker/components/TimePicker.module.css (8)
1-9
: Prefer design tokens over hard-coded values for consistency and themingReplace literal values with design-system tokens to align with existing
*.module.css
patterns and ensure dark-mode/theming support..calendarInputWrapper { display: flex; align-items: center; - border-radius: 4px; + border-radius: var(--ds-border-radius-md); border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-strong); gap: var(--ds-size-1); - background: white; - padding: 2px; + background: var(--ds-color-neutral-background-default); + padding: var(--ds-size-1); }
11-17
: Hover ring looks right, but consider token dedicated to focus/interactive rings
box-shadow
ring on hover uses accent-border-strong; if the DS has a specific interactive/hover ring token, prefer that for consistency across inputs. Otherwise, keep as-is.
19-39
: Focus outline offset is negative; consider positive offset to avoid clipping and improve accessibilityNegative
outline-offset
can clip the focus ring in some environments..segmentContainer input:focus-visible { - outline: 2px solid var(--ds-color-accent-border-strong); - outline-offset: -1px; + outline: 2px solid var(--ds-color-accent-border-strong); + outline-offset: 2px; border-radius: 2px; }
56-61
: Remove commented min-width or document why it’s needed
/*min-width: 320px;*/
suggests a previous constraint. Either delete or add a code comment explaining responsive rationale.
101-126
: Avoid !important; increase specificity insteadSignals potential specificity issues. Prefer a more specific selector to ensure selected state wins over hover, and duplicate the rule for
:hover
if necessary.-.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; -} +.dropdownOptionSelected, +.dropdownOptionSelected:hover { + background-color: var(--ds-color-accent-base-active); + color: var(--ds-color-neutral-text-on-inverted); + font-weight: 500; +}
118-138
: Outline color uses a text token; prefer a border/focus token if available
var(--ds-color-neutral-text-on-inverted)
for the outline is semantically a text color. If the DS has a*-border-strong
or*-focus
token for outlines on inverted surfaces, prefer that.
90-99
: Firefox scrollbar support and reduced-motionYou’ve styled WebKit scrollbars; add
scrollbar-color
for Firefox and respect reduced motion on hover transitions..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%; + /* Firefox scrollbar colors: thumb track */ + scrollbar-color: var(--ds-color-neutral-border-default) var(--ds-color-neutral-background-subtle); } .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; + transition: background-color 0.15s ease; } + +@media (prefers-reduced-motion: reduce) { + .dropdownOption { + transition: none; + } +}Also applies to: 150-167
1-18
: Class name reads “calendar” in a TimePicker stylesheet
calendarInputWrapper
may confuse future readers. If safe, rename to something time-specific (e.g.,timeInputWrapper
) and update usage.src/language/texts/nb.ts (1)
45-51
: Minor Bokmål grammar tweak for clarity“tidligst tillatte tid” → “tidligste tillatte tid” reads more natural; similarly ensure parallel phrasing for max. If product prefers “tidspunkt”, consider that for extra clarity.
- 'time_picker.invalid_time_message': 'Ugyldig tidsformat. Bruk formatet {0}.', - 'time_picker.min_time_exceeded': 'Tiden du har valgt er før tidligst tillatte tid ({0}).', - 'time_picker.max_time_exceeded': 'Tiden du har valgt er etter seneste tillatte tid ({0}).', + 'time_picker.invalid_time_message': 'Ugyldig tidsformat. Bruk formatet {0}.', + 'time_picker.min_time_exceeded': 'Tiden du har valgt er før tidligste tillatte tid ({0}).', + 'time_picker.max_time_exceeded': 'Tiden du har valgt er etter seneste tillatte tid ({0}).',Note: The keys mix
time_picker.*
(validation) andtimepicker.*
(labels). If intentional to mirror existingdate_picker.*
, fine. If not, we should unify naming in all locales in a follow-up to avoid future confusion.src/app-components/TimePicker/README.md (2)
11-17
: Tiny grammar/wording polish (optional)
- “Auto-coercion” → “Auto‑padding/coercion” for clarity.
- “moves to next segment” → “moves to the next segment”
- “Type ':', '.', ',' or space” → “Type ':', '.', ',', or space”
Also applies to: 20-24
136-142
: Accessibility: consider explicitly documenting ARIA relationshipsIf the dropdown is aria-controlled by the trigger, add a note recommending
aria-controls
andaria-expanded
on the trigger, androle="listbox"/option"
with proper keyboard support in the dropdown. This keeps implementers aligned with WAI-ARIA practices.src/app-components/TimePicker/tests/TimeSegment.test.tsx (2)
83-99
: Timer-backed behavior: make intent explicit or use fake timersSome segment commits depend on timeouts. While these tests avoid the 1s buffer by advancing/committing via navigation/blur, it’s worth documenting that to prevent accidental flakiness if tests change. Alternatively, wrap with fake timers where applicable.
Also applies to: 100-115, 134-153
241-255
: Avoid brittle getAllByRole indexingSelecting the second textbox by index can break if test order or render tree changes. Prefer scoping via
within(container)
or addingaria-label
specific to the period input.-const pmInput = screen.getAllByRole('textbox')[1]; +const pmInput = screen.getByRole('textbox', { name: /period/i });You may need to pass a specific
aria-label
when rendering that instance.src/app-components/TimePicker/tests/dropdownBehavior.test.ts (1)
111-139
: Page jump logic: clarify minute-step assumptions in a code comment
getPageJumpIndex
assumes pages = 60 / step. Consider documenting this behavior in the implementation to help future maintainers (e.g., that “PageUp/Down” jumps one hour worth of minute options).src/app-components/TimePicker/tests/keyboardNavigation.test.ts (3)
13-14
: Avoid duplicating exported types; import SegmentType from the source instead.Redefining SegmentType here risks drift if the production type changes. Import the type from the utils module to keep tests aligned with the implementation.
+import type { SegmentType } from 'src/app-components/TimePicker/utils/keyboardNavigation'; - -type SegmentType = 'hours' | 'minutes' | 'seconds' | 'period';
18-19
: Remove double type assertions and centralize a mock key event helper.The pattern
as unknown as MockKeyboardEvent
is brittle. Build a tiny factory that returns the exact shape the handler requires; it keeps tests tight and type-safe.+const mkKeyEvt = (key: string): MockKeyboardEvent => ({ key, preventDefault: jest.fn() }); - -const mockEvent = { key: 'ArrowUp', preventDefault: jest.fn() } as unknown as MockKeyboardEvent; +const mockEvent = mkKeyEvt('ArrowUp'); -const mockEvent = { key: 'ArrowDown', preventDefault: jest.fn() } as unknown as MockKeyboardEvent; +const mockEvent = mkKeyEvt('ArrowDown'); -const mockEvent = { key: 'ArrowRight', preventDefault: jest.fn() } as unknown as MockKeyboardEvent; +const mockEvent = mkKeyEvt('ArrowRight'); -const mockEvent = { key: 'ArrowLeft', preventDefault: jest.fn() } as unknown as MockKeyboardEvent; +const mockEvent = mkKeyEvt('ArrowLeft'); -const mockEvent = { key: 'Enter', preventDefault: jest.fn() } as unknown as MockKeyboardEvent; +const mockEvent = mkKeyEvt('Enter');Also applies to: 27-28, 35-36, 44-45, 53-54
199-218
: Add a decrement-with-gaps constraint test for symmetry.You test increment skipping invalid values; add the mirror for decrement to prevent regressions.
it('should skip invalid values when decrementing', () => { const constraints = { min: 8, max: 12, validValues: [8, 10, 12] }; // Missing 9, 11 const result = handleValueDecrement(12, 'hours', 'HH:mm', constraints); expect(result).toBe(10); });src/app-components/TimePicker/tests/segmentTyping.test.ts (2)
121-153
: Add buffer parsing tests for invalid input and seconds segment.Consider adding:
- processSegmentBuffer with non-numeric buffer for hours/minutes to assert it returns placeholder and isComplete=false.
- A seconds-specific case to ensure symmetry with minutes.
it('should handle non-numeric buffer', () => { expect(processSegmentBuffer('x', 'hours', false)).toEqual({ displayValue: '--', actualValue: null, isComplete: false, }); }); it('should handle seconds buffer like minutes', () => { expect(processSegmentBuffer('7', 'seconds', false)).toEqual({ displayValue: '07', actualValue: 7, isComplete: true, }); });
182-196
: Consider a 12-hour commit + coerce integration assertion.commitSegmentValue(null, 'hours') returns 0. In 12-hour flows, coerceToValidRange should then correct 0 → 1. A quick assertion helps document that contract.
expect(coerceToValidRange(commitSegmentValue(null, 'hours') as number, 'hours', true)).toBe(1);src/app-components/TimePicker/tests/typingBehavior.test.tsx (5)
30-31
: Prefer accessible queries over container.querySelector.Use role/label-based queries to match user-facing semantics and reduce brittleness.
-const hoursInput = container.querySelector('input[aria-label="Hours"]') as HTMLInputElement; +const hoursInput = screen.getByRole('textbox', { name: /hours/i }) as HTMLInputElement; -const minutesInput = container.querySelector('input[aria-label="Minutes"]') as HTMLInputElement; +const minutesInput = screen.getByRole('textbox', { name: /minutes/i }) as HTMLInputElement;Also applies to: 67-69, 99-103, 130-133, 168-174, 206-212, 233-239, 267-271, 303-310
37-43
: Replace keyPress with userEvent.type (keypress is deprecated and flaky in React 18).keyPress is deprecated and can diverge across environments. userEvent.type simulates real typing (keydown/press/input/keyup) and reduces flakiness.
- fireEvent.keyPress(hoursInput, { key: '2', charCode: 50 }); + await userEvent.type(hoursInput, '2'); - fireEvent.keyPress(minutesInput, { key: '5', charCode: 53 }); + await userEvent.type(minutesInput, '5');Apply similarly for all digit inputs in this file.
Also applies to: 71-77, 103-109, 134-141, 172-179, 209-214, 249-252, 272-279, 314-319
13-16
: Tighten fake timer cleanup.Clear pending timers before returning to real timers to avoid cross-test leakage.
afterEach(() => { jest.runOnlyPendingTimers(); + jest.clearAllTimers(); jest.useRealTimers(); });
49-54
: Wrap timer advances in act for React 18.While waitFor often covers act boundaries, explicitly wrapping timer advances in act prevents subtle scheduler warnings.
import { act } from 'react-dom/test-utils'; // ... await act(async () => { jest.advanceTimersByTime(1100); });Also applies to: 79-86, 111-117, 147-153, 181-191, 214-220, 283-288
50-51
: Avoid magic numbers; extract debounce to a constant used in both component and tests.If the component uses a debounce (e.g., 1000–1100ms), expose it or export a test-only constant to keep tests robust to tuning changes.
Also applies to: 80-81, 112-113, 148-149, 186-187
src/app-components/TimePicker/tests/timeConstraintUtils.test.ts (3)
8-24
: Don’t re-declare interfaces already exported by the module under test.Import the types to prevent duplication/drift and ensure the tests fail if the public API changes incompatibly.
-import { - getNextValidValue, - getSegmentConstraints, - isTimeInRange, - parseTimeString, -} from 'src/app-components/TimePicker/utils/timeConstraintUtils'; +import { + getNextValidValue, + getSegmentConstraints, + isTimeInRange, + parseTimeString, + type TimeValue, + type TimeConstraints, + type SegmentConstraints, +} from 'src/app-components/TimePicker/utils/timeConstraintUtils'; - -interface TimeValue { - hours: number; - minutes: number; - seconds: number; - period: 'AM' | 'PM'; -} - -interface TimeConstraints { - minTime?: string; - maxTime?: string; -} - -interface SegmentConstraints { - min: number; - max: number; - validValues: number[]; -}
151-166
: Add a complementary minutes-at-max-hour constraint case.You covered minTime on minutes; add the mirror for maxTime to assert upper bound clipping.
it('should constrain minutes when on maxTime hour', () => { const currentTime: TimeValue = { hours: 16, minutes: 0, seconds: 0, period: 'PM' }; const constraints = { maxTime: '16:15' }; const result = getSegmentConstraints('minutes', currentTime, constraints, 'HH:mm'); expect(result.validValues).toEqual(Array.from({ length: 16 }, (_, i) => i)); // 0..15 });
89-120
: Add equality-to-maxTime check.You asserted equals minTime; also assert equals maxTime to validate inclusive upper bound semantics.
it('should return true when time equals maxTime', () => { const sampleTime: TimeValue = { hours: 17, minutes: 0, seconds: 0, period: 'PM' }; const constraints = { minTime: '09:00', maxTime: '17:00' }; expect(isTimeInRange(sampleTime, constraints, 'HH:mm')).toBe(true); });src/app-components/TimePicker/tests/dropdownKeyboardNavigation.test.tsx (4)
15-19
: Restore scrollIntoView after tests to avoid cross-suite side effects.Mocking Element.prototype persists across tests. Restore it in afterEach.
beforeEach(() => { jest.clearAllMocks(); // Mock scrollIntoView Element.prototype.scrollIntoView = jest.fn(); }); + +afterEach(() => { + // Restore to a no-op function to prevent leakage across other suites + // eslint-disable-next-line @typescript-eslint/no-empty-function + Element.prototype.scrollIntoView = function () {}; +});
56-59
: Avoid brittle text selector for localized headers.Using getByText('Timer') ties the test to a specific locale string. Prefer selecting the hour option directly (as you already do) or add stable testids/roles to the column containers.
-// Selected hour should be visually highlighted -const hoursColumn = screen.getByText('Timer').parentElement; -const selectedHour = within(hoursColumn!).getByRole('button', { name: '14' }); +// Selected hour should be visually highlighted +const selectedHour = screen.getByRole('button', { name: '14' }); expect(selectedHour).toHaveClass('dropdownOptionSelected');
410-431
: scrollIntoView assertion: assert on the prototype mock to avoid element-bound ambiguity.When mocking the prototype, asserting on Element.prototype.scrollIntoView avoids confusion about which instance was called.
- expect(hour15.scrollIntoView).toHaveBeenCalledWith({ + expect(Element.prototype.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'nearest', });
557-565
: Space key name should be ' ' (space), not 'Space'.DOM KeyboardEvent.key for space is a single space character. Using 'Space' is non-standard and may cause confusion.
-const ignoredKeys = ['Tab', 'Space', 'a', '1', 'Backspace']; +const ignoredKeys = ['Tab', ' ', 'a', '1', 'Backspace'];src/app-components/TimePicker/tests/timeFormatUtils.test.ts (3)
8-13
: Prefer importing the shared TimeValue type to avoid driftDuplicate local interface risks divergence from the source of truth. Import the existing TimeValue instead.
Apply within this file:
-interface TimeValue { - hours: number; - minutes: number; - seconds: number; - period: 'AM' | 'PM'; -}And add this import near the top:
import type { TimeValue } from 'src/app-components/TimePicker/utils/timeConstraintUtils';
200-207
: Align hour padding semantics across component and utilsThese tests assert that 12‑hour strings are not zero‑padded (e.g., '9:05 AM'). In TimePickerComponent, displayValue currently pads the 12‑hour hour segment to two digits. Please align the component to match these semantics or adjust tests if the intended UX is to pad. I’ve proposed a component-side fix below.
112-120
: Add whitespace robustness tests for period parsingparseSegmentInput currently won’t accept inputs like ' am ' due to missing trim before comparison. Consider adding tests for whitespace-surrounded period inputs and address with the util fix I proposed in timeFormatUtils.ts.
Also applies to: 127-135
src/layout/TimePicker/TimePickerComponent.tsx (2)
68-85
: ISO detection heuristic is brittleUsing value.includes('T') to detect ISO-like values can misclassify non-ISO strings that contain 'T'. Prefer checking a stricter ISO pattern or attempting Date.parse with a guarded try before formatting.
117-119
: Confirm both disabled and readOnly props are neededIf TimePickerControl treats readOnly and disabled differently, carry both; otherwise, prefer a single prop to avoid divergent states.
src/app-components/TimePicker/utils/timeFormatUtils.ts (2)
60-66
: Trim period input before case normalizationWithout trimming, values like ' am ' won’t parse.
- const upperInput = input.toUpperCase(); + const upperInput = input.trim().toUpperCase();
28-33
: Optional: normalize period casing on outputFor consistency, return uppercase for the 'period' segment even if a lowercased string slips in.
- if (segmentType === 'period') { - return value.toString(); - } + if (segmentType === 'period') { + return value.toString().toUpperCase(); + }src/app-components/TimePicker/utils/dropdownBehavior.ts (2)
34-48
: Initial highlight may be wrong in 12h mode if options are 1–12 but getHours() returns 0–23getInitialHighlightIndex uses systemTime.getHours() directly. Ensure the options' value domain matches (e.g., 0–23 for 24h lists) or map to 12h before lookup when used with 12h hours lists.
I can add a format argument and normalize hours accordingly if needed.
112-125
: Safer nearest numeric lookup with heterogeneous optionsIf options include strings (e.g., 'AM', 'PM'), Number('AM') is NaN and the current logic happens to fall back to index 0. Prefer filtering numeric options for distance calculations to avoid NaN comparisons.
- // Find nearest numeric value - let nearestIndex = 0; - let nearestDiff = Math.abs(Number(options[0].value) - value); - - for (let i = 1; i < options.length; i++) { - const diff = Math.abs(Number(options[i].value) - value); - if (diff < nearestDiff) { - nearestDiff = diff; - nearestIndex = i; - } - } + // Find nearest numeric value + const numeric = options + .map((opt, i) => ({ i, v: typeof opt.value === 'number' ? opt.value : Number.NaN })) + .filter(({ v }) => Number.isFinite(v)); + if (numeric.length === 0) return 0; + let nearestIndex = numeric[0].i; + let nearestDiff = Math.abs(numeric[0].v - value); + for (let k = 1; k < numeric.length; k++) { + const diff = Math.abs(numeric[k].v - value); + if (diff < nearestDiff) { + nearestDiff = diff; + nearestIndex = numeric[k].i; + } + } return nearestIndex;src/app-components/TimePicker/utils/timeConstraintUtils.ts (2)
58-80
: Time range computation is correct; consider small refactor for readabilityMinor: compute timeInSeconds directly without the intermediate minutes var.
21-56
: Optional: centralize 12h⇄24h conversionparseTimeString embeds the conversion; extracting helpers would reduce duplication across modules and test fixtures.
src/app-components/TimePicker/components/TimeSegment.tsx (3)
272-279
: PreferonBeforeInput
/onKeyDown
overonKeyPress
(deprecated).
onKeyPress
is deprecated in modern React/DOM. Consider moving character handling toonBeforeInput
(for text insertion) and keeping navigation/increment ononKeyDown
. This improves IME and paste handling.Minimal change in render:
- onKeyPress={handleKeyPress} + onBeforeInput={handleBeforeInput} onKeyDown={handleKeyDown}And add a handler near other callbacks:
const handleBeforeInput = (e: React.FormEvent<HTMLInputElement> & { nativeEvent: InputEvent }) => { const ev = e.nativeEvent; if (ev.inputType === 'insertText' && typeof ev.data === 'string' && ev.data.length === 1) { e.preventDefault(); const char = ev.data; // reuse logic from handleKeyPress isTypingRef.current = true; const result = handleSegmentCharacterInput(char, type, segmentBuffer, format); if (result.shouldNavigate) { commitBuffer(true); onNavigate('right'); return; } setSegmentBuffer(result.newBuffer); bufferRef.current = result.newBuffer; const buffer = processSegmentBuffer(result.newBuffer, type, format.includes('a')); setLocalValue(buffer.displayValue); if (result.shouldAdvance) { if (buffer.actualValue !== null) { const committedValue = commitSegmentValue(buffer.actualValue, type); onValueChange(committedValue); } setSegmentBuffer(''); bufferRef.current = ''; isTypingRef.current = false; if (bufferTimeout) clearTimeout(bufferTimeout), setBufferTimeout(null); if (typingEndTimeout) clearTimeout(typingEndTimeout), setTypingEndTimeout(null); onNavigate('right'); } else { resetBufferTimeout(); } } };
295-297
: Redundant ternary formaxLength
.
type === 'period' ? 2 : 2
is always 2.Apply this diff:
- maxLength={type === 'period' ? 2 : 2} + maxLength={2}
136-170
: Arrow increment/decrement ignore constraints at the segment level.
handleValueIncrement/Decrement
are called without constraints, producing intermediate invalid values the parent later corrects. This causes extra renders and a janky UX around boundaries.If you pass the per-segment
validValues
tohandleValueIncrement/Decrement
, arrows will skip disabled values immediately. That requires adding aconstraints?: SegmentConstraints
prop orvalidValues?: number[]
toTimeSegmentProps
and wiring it fromTimePicker
. Want me to draft that end-to-end change?src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
6-12
:preventDefault
in result is redundant.You already call
event.preventDefault()
insidehandleSegmentKeyDown
. Returning apreventDefault
flag invites misuse and adds noise.Apply this diff:
export interface SegmentNavigationResult { shouldNavigate: boolean; direction?: 'left' | 'right'; shouldIncrement?: boolean; shouldDecrement?: boolean; - preventDefault: boolean; }
And drop the
preventDefault
fields in return objects.src/app-components/TimePicker/components/TimePicker.tsx (5)
281-292
: Useless initial assignment tonextIndex
(CodeQL).
nextIndex
is immediately overwritten. Simplify.Apply this diff:
- const handleSegmentNavigate = (direction: 'left' | 'right', currentIndex: number) => { - let nextIndex = currentIndex; - - if (direction === 'right') { - nextIndex = (currentIndex + 1) % segments.length; - } else { - nextIndex = (currentIndex - 1 + segments.length) % segments.length; - } + const handleSegmentNavigate = (direction: 'left' | 'right', currentIndex: number) => { + const nextIndex = + direction === 'right' + ? (currentIndex + 1) % segments.length + : (currentIndex - 1 + segments.length) % segments.length;
697-704
: Hardcoded Norwegian/English labels in dropdown; uselabels
prop for i18n consistency.Column headers use fixed strings ("Timer", "Minutter", "Sekunder", "AM/PM") while segments accept
labels
. This mixes locales and blocks translation.Apply this diff to reuse
segmentLabels
:- <div className={styles.dropdownLabel}>Timer</div> + <div className={styles.dropdownLabel}>{segmentLabels.hours}</div> ... - <div className={styles.dropdownLabel}>Minutter</div> + <div className={styles.dropdownLabel}>{segmentLabels.minutes}</div> ... - <div className={styles.dropdownLabel}>Sekunder</div> + <div className={styles.dropdownLabel}>{segmentLabels.seconds}</div> ... - <div className={styles.dropdownLabel}>AM/PM</div> + <div className={styles.dropdownLabel}>{segmentLabels.period}</div>Also applies to: 742-747, 781-785, 821-825
669-696
: Dialog a11y: add an accessible name.Popover content has
role='dialog'
but noaria-labelledby
oraria-label
. Provide an accessible name. If you don’t have a visible heading, addaria-label
on the dialog.Example:
- <Popover + <Popover ref={dropdownRef} className={styles.timePickerDropdown} aria-modal - aria-hidden={!showDropdown} + aria-hidden={!showDropdown} role='dialog' open={showDropdown} + aria-label='Time picker'Or reference a visible heading element by id via
aria-labelledby
.Also applies to: 681-694
15-36
:aria-labelledby?: never
unnecessarily forbids a common a11y pattern.Disallowing
aria-labelledby
prevents integrating with external labels. Consider allowing it alongsidearia-label
.If this was intentional (design system constraint), ignore. Otherwise, remove
never
and let standard ARIA patterns work.
409-445
: AM/PM enablement not constrained; period may be selectable even when all hours in that period are invalid.Cases with tight
minTime
/maxTime
can make an entire period invalid (e.g., only morning times allowed). AM/PM buttons are always enabled.We can disable the AM/PM option if switching would yield no valid hour/minute/second combination. I can implement a helper that probes
getSegmentConstraints
for the target period and returns disabled whenhours.validValues
is empty (and minutes/seconds compatible). Want me to draft it?src/app-components/TimePicker/utils/segmentTyping.ts (1)
114-116
:Tab
inisNavigationKey
is ineffective here and could be misleading.
isNavigationKey
includes'Tab'
, but you only call it fromonKeyPress
(or plannedonBeforeInput
).Tab
is processed on keydown and will not be handled here.Either remove
'Tab'
fromisNavigationKey
or handleTab
in the keydown handler (committing buffer before default tabbing).Also applies to: 244-249
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (23)
src/app-components/TimePicker/README.md
(1 hunks)src/app-components/TimePicker/components/TimePicker.module.css
(1 hunks)src/app-components/TimePicker/components/TimePicker.tsx
(1 hunks)src/app-components/TimePicker/components/TimeSegment.tsx
(1 hunks)src/app-components/TimePicker/tests/TimeSegment.test.tsx
(1 hunks)src/app-components/TimePicker/tests/dropdownBehavior.test.ts
(1 hunks)src/app-components/TimePicker/tests/dropdownKeyboardNavigation.test.tsx
(1 hunks)src/app-components/TimePicker/tests/keyboardNavigation.test.ts
(1 hunks)src/app-components/TimePicker/tests/segmentTyping.test.ts
(1 hunks)src/app-components/TimePicker/tests/timeConstraintUtils.test.ts
(1 hunks)src/app-components/TimePicker/tests/timeFormatUtils.test.ts
(1 hunks)src/app-components/TimePicker/tests/typingBehavior.test.tsx
(1 hunks)src/app-components/TimePicker/utils/dropdownBehavior.ts
(1 hunks)src/app-components/TimePicker/utils/keyboardNavigation.ts
(1 hunks)src/app-components/TimePicker/utils/segmentTyping.ts
(1 hunks)src/app-components/TimePicker/utils/timeConstraintUtils.ts
(1 hunks)src/app-components/TimePicker/utils/timeFormatUtils.ts
(1 hunks)src/language/texts/en.ts
(1 hunks)src/language/texts/nb.ts
(1 hunks)src/language/texts/nn.ts
(1 hunks)src/layout/TimePicker/TimePickerComponent.test.tsx
(1 hunks)src/layout/TimePicker/TimePickerComponent.tsx
(1 hunks)src/layout/TimePicker/useTimePickerValidation.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/language/texts/en.ts
- src/layout/TimePicker/TimePickerComponent.test.tsx
- src/language/texts/nn.ts
- src/layout/TimePicker/useTimePickerValidation.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/app-components/TimePicker/tests/timeFormatUtils.test.ts
src/app-components/TimePicker/tests/timeConstraintUtils.test.ts
src/app-components/TimePicker/utils/timeConstraintUtils.ts
src/app-components/TimePicker/tests/segmentTyping.test.ts
src/app-components/TimePicker/utils/dropdownBehavior.ts
src/app-components/TimePicker/tests/dropdownBehavior.test.ts
src/app-components/TimePicker/utils/keyboardNavigation.ts
src/app-components/TimePicker/utils/segmentTyping.ts
src/app-components/TimePicker/tests/TimeSegment.test.tsx
src/app-components/TimePicker/tests/typingBehavior.test.tsx
src/app-components/TimePicker/tests/keyboardNavigation.test.ts
src/app-components/TimePicker/tests/dropdownKeyboardNavigation.test.tsx
src/app-components/TimePicker/utils/timeFormatUtils.ts
src/app-components/TimePicker/components/TimeSegment.tsx
src/language/texts/nb.ts
src/layout/TimePicker/TimePickerComponent.tsx
src/app-components/TimePicker/components/TimePicker.tsx
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In tests, use
renderWithProviders
fromsrc/test/renderWithProviders.tsx
to supply required form layout context
Files:
src/app-components/TimePicker/tests/timeFormatUtils.test.ts
src/app-components/TimePicker/tests/timeConstraintUtils.test.ts
src/app-components/TimePicker/tests/segmentTyping.test.ts
src/app-components/TimePicker/tests/dropdownBehavior.test.ts
src/app-components/TimePicker/tests/TimeSegment.test.tsx
src/app-components/TimePicker/tests/typingBehavior.test.tsx
src/app-components/TimePicker/tests/keyboardNavigation.test.ts
src/app-components/TimePicker/tests/dropdownKeyboardNavigation.test.tsx
**/*.module.css
📄 CodeRabbit inference engine (CLAUDE.md)
Use CSS Modules for component styling and follow existing patterns in
*.module.css
files
Files:
src/app-components/TimePicker/components/TimePicker.module.css
🧠 Learnings (1)
📚 Learning: 2025-08-22T13:53:28.201Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.201Z
Learning: Applies to **/*.module.css : Use CSS Modules for component styling and follow existing patterns in `*.module.css` files
Applied to files:
src/app-components/TimePicker/components/TimePicker.module.css
🧬 Code graph analysis (16)
src/app-components/TimePicker/tests/timeFormatUtils.test.ts (2)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
TimeValue
(3-8)src/app-components/TimePicker/utils/timeFormatUtils.ts (4)
formatTimeValue
(5-26)formatSegmentValue
(28-49)parseSegmentInput
(51-75)isValidSegmentInput
(77-117)
src/app-components/TimePicker/tests/timeConstraintUtils.test.ts (1)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (7)
TimeValue
(3-8)TimeConstraints
(10-13)SegmentConstraints
(15-19)parseTimeString
(21-56)isTimeInRange
(58-80)getSegmentConstraints
(82-189)getNextValidValue
(191-217)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
src/app-components/TimePicker/components/TimePicker.tsx (1)
TimeFormat
(13-13)
src/app-components/TimePicker/tests/segmentTyping.test.ts (1)
src/app-components/TimePicker/utils/segmentTyping.ts (9)
processHourInput
(18-67)processMinuteInput
(72-95)processPeriodInput
(100-109)processSegmentBuffer
(120-153)isNavigationKey
(114-115)clearSegment
(158-161)commitSegmentValue
(166-174)coerceToValidRange
(179-205)shouldAdvanceSegment
(210-226)
src/app-components/TimePicker/utils/dropdownBehavior.ts (1)
src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
SegmentType
(4-4)
src/app-components/TimePicker/tests/dropdownBehavior.test.ts (1)
src/app-components/TimePicker/utils/dropdownBehavior.ts (9)
roundToStep
(11-16)getInitialHighlightIndex
(21-51)getNextIndex
(56-62)getPageJumpIndex
(67-81)getHomeIndex
(86-86)getEndIndex
(91-91)findNearestOptionIndex
(96-125)calculateScrollPosition
(130-137)shouldScrollToOption
(142-157)
src/app-components/TimePicker/utils/keyboardNavigation.ts (2)
src/app-components/TimePicker/components/TimePicker.tsx (1)
TimeFormat
(13-13)src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
SegmentConstraints
(15-19)
src/app-components/TimePicker/utils/segmentTyping.ts (2)
src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
SegmentType
(4-4)src/app-components/TimePicker/components/TimePicker.tsx (1)
TimeFormat
(13-13)
src/app-components/TimePicker/tests/TimeSegment.test.tsx (1)
src/app-components/TimePicker/components/TimeSegment.tsx (2)
TimeSegmentProps
(20-36)TimeSegment
(38-300)
src/app-components/TimePicker/tests/typingBehavior.test.tsx (1)
src/app-components/TimePicker/components/TimePicker.tsx (1)
TimePicker
(87-854)
src/app-components/TimePicker/tests/keyboardNavigation.test.ts (1)
src/app-components/TimePicker/utils/keyboardNavigation.ts (5)
SegmentType
(4-4)handleSegmentKeyDown
(14-58)getNextSegmentIndex
(60-72)handleValueIncrement
(74-110)handleValueDecrement
(112-148)
src/app-components/TimePicker/tests/dropdownKeyboardNavigation.test.tsx (1)
src/app-components/TimePicker/components/TimePicker.tsx (1)
TimePicker
(87-854)
src/app-components/TimePicker/utils/timeFormatUtils.ts (3)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
TimeValue
(3-8)src/app-components/TimePicker/components/TimePicker.tsx (1)
TimeFormat
(13-13)src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
SegmentType
(4-4)
src/app-components/TimePicker/components/TimeSegment.tsx (4)
src/app-components/TimePicker/utils/keyboardNavigation.ts (4)
SegmentType
(4-4)handleSegmentKeyDown
(14-58)handleValueIncrement
(74-110)handleValueDecrement
(112-148)src/app-components/TimePicker/components/TimePicker.tsx (1)
TimeFormat
(13-13)src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
formatSegmentValue
(28-49)src/app-components/TimePicker/utils/segmentTyping.ts (4)
processSegmentBuffer
(120-153)commitSegmentValue
(166-174)clearSegment
(158-161)handleSegmentCharacterInput
(231-285)
src/layout/TimePicker/TimePickerComponent.tsx (7)
src/layout/index.ts (1)
PropsFromGenericComponent
(28-32)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/features/formData/useDataModelBindings.ts (1)
useDataModelBindings
(42-57)src/features/language/useLanguage.ts (1)
useLanguage
(90-93)src/utils/layout/useLabel.tsx (1)
useLabel
(13-72)src/layout/ComponentStructureWrapper.tsx (1)
ComponentStructureWrapper
(20-48)src/app-components/Flex/Flex.tsx (1)
Flex
(25-84)
src/app-components/TimePicker/components/TimePicker.tsx (4)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (4)
parseTimeString
(21-56)TimeValue
(3-8)TimeConstraints
(10-13)getSegmentConstraints
(82-189)src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
SegmentType
(4-4)src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
formatTimeValue
(5-26)src/app-components/TimePicker/components/TimeSegment.tsx (1)
TimeSegment
(38-300)
🪛 GitHub Check: CodeQL
src/app-components/TimePicker/components/TimePicker.tsx
[warning] 282-282: Useless assignment to local variable
The initial value of nextIndex is unused, since it is always overwritten.
🪛 LanguageTool
src/app-components/TimePicker/README.md
[grammar] ~16-~16: There might be a mistake here.
Context: ...- Auto-advance: Automatically moves to next segment when current segment is co...
(QB_NEW_EN)
[grammar] ~16-~16: There might be a mistake here.
Context: ...**: Automatically moves to next segment when current segment is complete ### Keyboa...
(QB_NEW_EN)
[grammar] ~23-~23: There might be a mistake here.
Context: ... Type ":", ".", "," or space to advance to next segment ### Format Support - **2...
(QB_NEW_EN)
[grammar] ~27-~27: There might be a mistake here.
Context: ...24-hour format*: "HH:mm" or "HH:mm:ss" - 12-hour format: "HH:mm a" or "HH:mm:ss...
(QB_NEW_EN)
[grammar] ~28-~28: There might be a mistake here.
Context: ...: "HH:mm a" or "HH:mm:ss a" (with AM/PM) - Flexible display: Configurable time fo...
(QB_NEW_EN)
[grammar] ~83-~83: There might be a mistake here.
Context: ...t for hours, minutes, seconds, or period - Implements Chrome-like typing behavior w...
(QB_NEW_EN)
[grammar] ~84-~84: There might be a mistake here.
Context: ...e typing behavior with buffer management - Handles keyboard navigation and value co...
(QB_NEW_EN)
[grammar] ~91-~91: There might be a mistake here.
Context: ...ercion logic for different segment types - Buffer Management: Handles multi-chara...
(QB_NEW_EN)
[grammar] ~92-~92: There might be a mistake here.
Context: ...dles multi-character input with timeouts - Validation: Ensures values stay within...
(QB_NEW_EN)
[grammar] ~97-~97: There might be a mistake here.
Context: ...*: Arrow key navigation between segments - Value Manipulation: Increment/decremen...
(QB_NEW_EN)
[grammar] ~98-~98: There might be a mistake here.
Context: ...n**: Increment/decrement with arrow keys - Key Handling: Special key processing (...
(QB_NEW_EN)
[grammar] ~111-~111: There might be a mistake here.
Context: ...for second digit, 3-9 auto-coerces to 0X - 12-hour mode: First digit 0-1 waits fo...
(QB_NEW_EN)
[grammar] ~112-~112: There might be a mistake here.
Context: ...for second digit, 2-9 auto-coerces to 0X - Second digit: Validates against first ...
(QB_NEW_EN)
[grammar] ~117-~117: There might be a mistake here.
Context: ...for second digit, 6-9 auto-coerces to 0X - Second digit: Always accepts 0-9 - **O...
(QB_NEW_EN)
[grammar] ~118-~118: There might be a mistake here.
Context: ...X - Second digit: Always accepts 0-9 - Overflow handling: Values > 59 are cor...
(QB_NEW_EN)
[grammar] ~123-~123: There might be a mistake here.
Context: ...Input (AM/PM) - A/a key: Sets to AM - P/p key: Sets to PM - **Case insensiti...
(QB_NEW_EN)
[grammar] ~124-~124: There might be a mistake here.
Context: ...**: Sets to AM - P/p key: Sets to PM - Case insensitive: Accepts both upper a...
(QB_NEW_EN)
[grammar] ~138-~138: There might be a mistake here.
Context: ...bility - ARIA Labels: Each segment has descriptive aria-label - **Keyboard Nav...
(QB_NEW_EN)
[grammar] ~147-~147: There might be a mistake here.
Context: ...*: Various input patterns and edge cases - Navigation: Keyboard navigation betwee...
(QB_NEW_EN)
[grammar] ~148-~148: There might be a mistake here.
Context: ...**: Keyboard navigation between segments - Buffer Management: Race condition prev...
(QB_NEW_EN)
[grammar] ~149-~149: There might be a mistake here.
Context: ...ondition prevention and timeout handling - Format Support: Different time formats...
(QB_NEW_EN)
[grammar] ~150-~150: There might be a mistake here.
Context: ...*: Different time formats and validation - Accessibility: Screen reader compatibi...
(QB_NEW_EN)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
- GitHub Check: Analyze (javascript)
- GitHub Check: Install
🔇 Additional comments (16)
src/app-components/TimePicker/tests/TimeSegment.test.tsx (2)
1-19
: Guideline check: renderWithProviders only if neededThese are unit tests for a leaf component with no app context. If the component later starts reading from context, switch to
renderWithProviders
per repo guidelines. For now,render
is fine.
3-5
: The scripts above will print the full test file (first 200 lines) and locate anyuserEvent.
usage to confirm where the v14 API refactor is needed. Once you provide the output, I can verify and draft the updated review comment.src/app-components/TimePicker/tests/dropdownBehavior.test.ts (1)
1-12
: LGTM: Clean, deterministic coverage of dropdown utilitiesGood breadth across rounding, navigation, paging, home/end, nearest, and scroll math. Test data is straightforward and readable.
src/app-components/TimePicker/tests/keyboardNavigation.test.ts (1)
15-62
: Overall: solid coverage and intent tests for navigation.Coverage across arrows, wrapping, 12/24h, and constraint-aware increments/decrements looks good and maps cleanly to the utility semantics.
Also applies to: 64-98, 100-150, 152-197
src/app-components/TimePicker/tests/segmentTyping.test.ts (2)
73-100
: Nice, readable minute typing behavior coverage.The cases mirror browser behavior and the utility logic closely.
13-71
: Overall: strong segmentation logic tests.The hour typing edge handling for 12/24h, navigation key detection, and range coercion scenarios are well covered.
Also applies to: 102-171, 173-181, 198-243
src/app-components/TimePicker/tests/typingBehavior.test.tsx (1)
7-12
: Overall: excellent behavioral coverage for the “no initial value” bug and buffer semantics.Scenarios around overwrites, parent updates, and blur/focus are thorough.
Also applies to: 18-28, 56-86, 119-154, 156-191, 194-221, 223-253, 255-320
src/app-components/TimePicker/tests/timeConstraintUtils.test.ts (1)
26-87
: Solid baseline coverage for parsing and core constraints.Parsing 12/24h, seconds, inclusive range checks, and hour/minute constraints are well-exercised.
Also applies to: 122-166, 168-218
src/app-components/TimePicker/tests/dropdownKeyboardNavigation.test.tsx (1)
45-79
: End-to-end dropdown keyboard coverage is comprehensive.Opening/closing semantics, focus restoration, wrapping, constraints, and 12h specifics are well validated.
Also applies to: 81-215, 217-351, 353-475, 477-531, 533-569
src/app-components/TimePicker/tests/timeFormatUtils.test.ts (1)
1-207
: Good coverage and edge casesOverall, the suite exercises formatting, segment formatting, parsing, validation, and boundaries well. Nice job covering noon/midnight and partial inputs.
Also applies to: 209-217
src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
5-26
: Utilities look solid and match testsClear separation of formatting, segment formatting, parsing, and validation with sensible handling of partial inputs and ranges.
Also applies to: 28-49, 51-75, 77-117
src/app-components/TimePicker/utils/dropdownBehavior.ts (1)
1-16
: Navigation and scrolling helpers look goodRound/next/home/end/scrolling logic is straightforward and clear.
Also applies to: 21-51, 53-63, 83-92, 130-137, 142-157
src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
82-131
: Constraints logic is clear and consistent across segmentsGood normalization of 12h/24h bounds and contextual minute/second limits at boundary hours/minutes.
Also applies to: 133-189, 191-217
src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
74-110
: LGTM: wrapped increment semantics are correct and align with 12h/24h rules.Default wrapping for hours/minutes/seconds and AM/PM toggling look good.
src/app-components/TimePicker/utils/segmentTyping.ts (2)
18-66
: Hour input coercion mirrors native behavior—nice.Smart handling of first/second digit across 12h/24h is solid and matches Chrome-like expectations.
176-205
: Ignore removal suggestion: functions are used in testsI confirmed that both
coerceToValidRange
andshouldAdvanceSegment
are explicitly imported and exercised insrc/app-components/TimePicker/tests/segmentTyping.test.ts
. They are not unused—removing them would break the existing tests. If you still want to centralize clamping and auto-advance behavior intoTimeSegment
, feel free to invoke these helpers there, but they must remain defined for the current test suite.Likely an incorrect or invalid review comment.
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.
Actionable comments posted: 0
🧹 Nitpick comments (8)
src/app-components/TimePicker/tests/dropdownBehavior.test.ts (8)
1-7
: Imports look good; consider preemptively importing remaining utils when you add tests for them.If you expand coverage to
getPageJumpIndex
,getHomeIndex
,getEndIndex
, orshouldScrollToOption
, add them here to keep imports centralized.
10-23
: Solid coverage for roundToStep; add non-finite step edge cases.You already cover zero/negative steps. Add NaN/Infinity cases to lock in the guard on non-finite steps.
Apply this diff within the same
describe('roundToStep', ...)
block:it('should handle gracefully with invalid step', () => { expect(roundToStep(7, 0)).toBe(7); // Invalid step, return value expect(roundToStep(7, -1)).toBe(7); // Invalid step, return value }); + + it('should return original value for non-finite steps (NaN/Infinity)', () => { + expect(roundToStep(7, Number.NaN)).toBe(7); + expect(roundToStep(7, Number.POSITIVE_INFINITY)).toBe(7); + });
25-53
: Add tests for system-time fallback and step rounding in getInitialHighlightIndex.The branch that uses
systemTime
+segmentType
+step
isn’t exercised. Add hours/minutes fallback tests (including minute rounding), and verify thatperiod
returns 0 when using system time.Apply this diff inside the same
describe('getInitialHighlightIndex', ...)
block:it('should return 0 when no match found', () => { expect(getInitialHighlightIndex(99, hourOptions)).toBe(0); }); + + it('should use system time (hours) when no current value', () => { + const systemTime = new Date(2020, 0, 1, 13, 17); // 13:17 + // Note: step is required by the util even for hours; use 1 + expect(getInitialHighlightIndex(null, hourOptions, 'hours', 1, systemTime)).toBe(13); + }); + + it('should use rounded system minutes when no current value', () => { + const systemTime = new Date(2020, 0, 1, 13, 17); // 17 -> rounds to 15 with step 5 + // minuteOptions uses 5-minute steps; 15 is at index 3 + expect(getInitialHighlightIndex(null, minuteOptions, 'minutes', 5, systemTime)).toBe(3); + }); + + it('should return 0 for period segment when using system time', () => { + const periodOptions = [ + { value: 'AM', label: 'AM' }, + { value: 'PM', label: 'PM' }, + ]; + expect(getInitialHighlightIndex(null, periodOptions, 'period', 1, new Date())).toBe(0); + }); + + it('should fall back to 0 if rounded system minute is not in options', () => { + const systemTime = new Date(2020, 0, 1, 13, 17); // 17 -> rounds to 21 with step 7, not present + expect(getInitialHighlightIndex(null, minuteOptions, 'minutes', 7, systemTime)).toBe(0); + });
55-62
: Good bounds checks; add a single-option edge case test.One-item lists are a common boundary; asserting both directions stay at 0 will make the intent explicit.
Apply this diff within the same
describe('getNextIndex', ...)
block:it('should move up and down correctly', () => { expect(getNextIndex(5, 'up', 10)).toBe(4); expect(getNextIndex(5, 'down', 10)).toBe(6); expect(getNextIndex(0, 'up', 10)).toBe(0); // Can't go below 0 expect(getNextIndex(9, 'down', 10)).toBe(9); // Can't go above max }); + + it('should clamp within bounds for a single option', () => { + expect(getNextIndex(0, 'up', 1)).toBe(0); + expect(getNextIndex(0, 'down', 1)).toBe(0); + });
64-90
: Nearest-index tests are solid; consider empty/string-miss and tie behavior.
- Empty options should return 0.
- For string values with no match, util returns 0.
- Tie distance should prefer the lower index (current implementation uses strict
<
).Apply this diff inside the same
describe('findNearestOptionIndex', ...)
block:it('should handle string values', () => { const periodOptions = [ { value: 'AM', label: 'AM' }, { value: 'PM', label: 'PM' }, ]; expect(findNearestOptionIndex('PM', periodOptions)).toBe(1); }); + + it('should return 0 for empty options', () => { + expect(findNearestOptionIndex(10, [])).toBe(0); + }); + + it('should return 0 for non-matching string values', () => { + const periodOptions = [ + { value: 'AM', label: 'AM' }, + { value: 'PM', label: 'PM' }, + ]; + expect(findNearestOptionIndex('XX', periodOptions)).toBe(0); + }); + + it('should prefer the lower index on equal distance (tie)', () => { + const evenOptions = [ + { value: 0, label: '00' }, // index 0, diff 5 + { value: 10, label: '10' }, // index 1, diff 5 + ]; + expect(findNearestOptionIndex(5, evenOptions)).toBe(0); + });
92-102
: Scroll centering cases look good; add equal container/item height edge case.This validates the centering arithmetic when
containerHeight === itemHeight
.Apply this diff within the same
describe('calculateScrollPosition', ...)
block:it('should not scroll negative', () => { expect(calculateScrollPosition(1, 400, 40)).toBe(0); }); + + it('should center correctly when container height equals item height', () => { + expect(calculateScrollPosition(1, 40, 40)).toBe(40); + });
9-103
: Overall: clear, focused util tests; component provider not needed here.This file tests pure utility functions—no need for
renderWithProviders
per our test guideline, so you’re aligned. Nice coverage of typical and boundary cases; the added edge cases above will round it out.
1-103
: Add unit tests for all exported dropdownBehavior helpersOur verification shows that several helpers in
src/app-components/TimePicker/utils/dropdownBehavior.ts
are exported but lack test coverage:
getPageJumpIndex
(export at line 67)getHomeIndex
(export at line 86)getEndIndex
(export at line 91)shouldScrollToOption
(export at line 142)Please add corresponding unit tests—either in
dropdownBehavior.test.ts
or in a dedicated test file—to ensure these functions behave as intended.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
src/app-components/TimePicker/tests/dropdownBehavior.test.ts
(1 hunks)src/app-components/TimePicker/tests/keyboardNavigation.test.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app-components/TimePicker/tests/keyboardNavigation.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/app-components/TimePicker/tests/dropdownBehavior.test.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In tests, use
renderWithProviders
fromsrc/test/renderWithProviders.tsx
to supply required form layout context
Files:
src/app-components/TimePicker/tests/dropdownBehavior.test.ts
🧬 Code graph analysis (1)
src/app-components/TimePicker/tests/dropdownBehavior.test.ts (1)
src/app-components/TimePicker/utils/dropdownBehavior.ts (5)
roundToStep
(11-16)getInitialHighlightIndex
(21-51)getNextIndex
(56-62)findNearestOptionIndex
(96-125)calculateScrollPosition
(130-137)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Analyze (javascript)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
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.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/app-components/TimePicker/components/TimePicker.tsx (2)
250-256
: Remove redundant initialization ofnextIndex
.Initial value is always overwritten on the following branch; simplify.
- let nextIndex = currentIndex; - - if (direction === 'right') { - nextIndex = (currentIndex + 1) % segments.length; - } else { - nextIndex = (currentIndex - 1 + segments.length) % segments.length; - } + const nextIndex = + direction === 'right' + ? (currentIndex + 1) % segments.length + : (currentIndex - 1 + segments.length) % segments.length;
383-396
: Hour disabling logic compares 24h candidates to a 12h valid set; options get wrongly disabled.
getSegmentConstraints('hours', ...)
returns 1–12 in 12‑hour formats, but the code derives a 24hactualHour
beforeincludes(...)
. Compare using the option’s native 12h value. Also, reuseisOptionDisabled
in the render to avoid duplicated logic.Apply these diffs:
case 0: { - // Hours - const hourValue = typeof optionValue === 'number' ? optionValue : parseInt(optionValue.toString(), 10); - let actualHour = hourValue; - if (is12Hour) { - if (timeValue.period === 'AM' && hourValue === 12) { - actualHour = 0; - } else if (timeValue.period === 'PM' && hourValue !== 12) { - actualHour = hourValue + 12; - } - } - return !getSegmentConstraints('hours', timeValue, constraints, format).validValues.includes(actualHour); + // Hours — compare in the segment's own domain (1–12 for 12h, 0–23 for 24h) + const hourValue = typeof optionValue === 'number' ? optionValue : parseInt(optionValue.toString(), 10); + const hoursValid = getSegmentConstraints('hours', timeValue, constraints, format).validValues; + return !hoursValid.includes(hourValue); }- const isDisabled = - constraints.minTime || constraints.maxTime - ? !getSegmentConstraints('hours', timeValue, constraints, format).validValues.includes( - is12Hour - ? option.value === 12 - ? timeValue.period === 'AM' - ? 0 - : 12 - : timeValue.period === 'PM' && option.value !== 12 - ? option.value + 12 - : option.value - : option.value, - ) - : false; + const isDisabled = isOptionDisabled(0, option.value);Follow‑up: For ranges spanning AM→PM (e.g., 11:30 AM–1:15 PM), the current constraints builder returns a non-wrapping 12h window that can be empty or misleading. Consider a helper that evaluates a 24h candidate against
minTime
/maxTime
inclusively to capture edge minutes/seconds across period boundaries. I can draft this if you want it in this PR.Also applies to: 650-664
🧹 Nitpick comments (6)
src/app-components/TimePicker/components/TimePicker.tsx (6)
8-8
: Deduplicate time parsing: import the sharedparseTimeString
util instead of reimplementing locally.Prevents logic drift and keeps all parsing rules in one place.
Apply these diffs:
- import { getSegmentConstraints } from 'src/app-components/TimePicker/utils/timeConstraintUtils'; + import { getSegmentConstraints, parseTimeString } from 'src/app-components/TimePicker/utils/timeConstraintUtils';-const parseTimeString = (timeStr: string, format: TimeFormat): TimeValue => { - const defaultValue: TimeValue = { hours: 0, minutes: 0, seconds: 0, period: 'AM' }; - - if (!timeStr) { - return defaultValue; - } - - const is12Hour = format.includes('a'); - const includesSeconds = format.includes('ss'); - - const parts = timeStr.replace(/\s*(AM|PM)/i, '').split(':'); - const periodMatch = timeStr.match(/(AM|PM)/i); - - const hours = parseInt(parts[0] || '0', 10); - const minutes = parseInt(parts[1] || '0', 10); - const seconds = includesSeconds ? parseInt(parts[2] || '0', 10) : 0; - const period = periodMatch ? (periodMatch[1].toUpperCase() as 'AM' | 'PM') : 'AM'; - - let actualHours = isNaN(hours) ? 0 : hours; - - if (is12Hour && !isNaN(hours)) { - // Parse 12-hour format properly - if (period === 'AM' && actualHours === 12) { - actualHours = 0; - } else if (period === 'PM' && actualHours !== 12) { - actualHours += 12; - } - } - - return { - hours: actualHours, - minutes: isNaN(minutes) ? 0 : minutes, - seconds: isNaN(seconds) ? 0 : seconds, - period: is12Hour ? period : 'AM', - }; -}; +// Use shared parseTimeString from utilsAlso applies to: 32-68
565-573
: Period change can yield out‑of‑range minutes/seconds; clamp to nearest valid per constraints.Switching AM/PM adjusts hours but doesn’t revalidate minutes/seconds, so the resulting time can fall outside the allowed window.
Apply this diff to clamp minutes/seconds after changing the period:
const handleDropdownPeriodChange = (period: 'AM' | 'PM') => { let newHours = timeValue.hours; if (period === 'PM' && timeValue.hours < 12) { newHours += 12; } else if (period === 'AM' && timeValue.hours >= 12) { newHours -= 12; } - updateTime({ period, hours: newHours }); + const base = { ...timeValue, hours: newHours }; + const minuteValid = getSegmentConstraints('minutes', base, constraints, format).validValues; + const nextMinutes = minuteValid.length && minuteValid.includes(base.minutes) ? base.minutes : (minuteValid[0] ?? 0); + const secondValid = getSegmentConstraints('seconds', { ...base, minutes: nextMinutes }, constraints, format).validValues; + const nextSeconds = secondValid.length && secondValid.includes(base.seconds) ? base.seconds : (secondValid[0] ?? 0); + updateTime({ period, hours: newHours, minutes: nextMinutes, seconds: nextSeconds }); };
24-30
: Expose/localize trigger button label via props.Avoid hardcoded English and let integrators localize it.
export interface TimePickerProps { id: string; value: string; onChange: (time: string) => void; format?: TimeFormat; minTime?: string; maxTime?: string; disabled?: boolean; readOnly?: boolean; labels?: { hours?: string; minutes?: string; seconds?: string; amPm?: string; + openPicker?: string; }; }
- aria-label='Open time picker' + aria-label={labels.openPicker || 'Open time picker'}Also applies to: 622-623
645-646
: Use the same labels for dropdown column headers to avoid mixed locales.Currently “Timer/Minutter/Sekunder” are hardcoded (Norwegian) while segment labels default to English. Reuse
segmentLabels
.- <div className={styles.dropdownLabel}>Timer</div> + <div className={styles.dropdownLabel}>{segmentLabels.hours}</div>- <div className={styles.dropdownLabel}>Minutter</div> + <div className={styles.dropdownLabel}>{segmentLabels.minutes}</div>- <div className={styles.dropdownLabel}>Sekunder</div> + <div className={styles.dropdownLabel}>{segmentLabels.seconds}</div>- <div className={styles.dropdownLabel}>AM/PM</div> + <div className={styles.dropdownLabel}>{segmentLabels.period}</div>Also applies to: 691-692, 730-731, 770-771
535-541
: Micro-optimization: memoize options arrays to avoid reallocation on every render.Low impact but easy win when the component re-renders frequently.
Example:
const hourOptions = useMemo( () => (is12Hour ? Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: (i + 1).toString().padStart(2, '0') })) : Array.from({ length: 24 }, (_, i) => ({ value: i, label: i.toString().padStart(2, '0') }))), [is12Hour], ); const minuteOptions = useMemo(() => Array.from({ length: 60 }, (_, i) => ({ value: i, label: i.toString().padStart(2, '0') })), []); const secondOptions = useMemo(() => Array.from({ length: 60 }, (_, i) => ({ value: i, label: i.toString().padStart(2, '0') })), []);
642-799
: Consider disabling AM/PM options when the entire period is invalid under constraints.Currently AM/PM is never disabled, so users can select a period that immediately forces hour/minute corrections. For better UX, compute if any hour within that period is valid and disable the period if none is.
I can help implement an
isPeriodDisabled(period)
using the constraints util to probe for any valid hour/minute/second in that period and wire it into the AM/PM buttons.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/app-components/TimePicker/components/TimePicker.module.css
(1 hunks)src/app-components/TimePicker/components/TimePicker.tsx
(1 hunks)src/layout/TimePicker/TimePickerComponent.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/layout/TimePicker/TimePickerComponent.tsx
- src/app-components/TimePicker/components/TimePicker.module.css
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/app-components/TimePicker/components/TimePicker.tsx
🧬 Code graph analysis (1)
src/app-components/TimePicker/components/TimePicker.tsx (4)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (4)
parseTimeString
(21-56)TimeValue
(3-8)TimeConstraints
(10-13)getSegmentConstraints
(82-189)src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
SegmentType
(4-4)src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
formatTimeValue
(5-26)src/app-components/TimePicker/components/TimeSegment.tsx (1)
TimeSegment
(38-300)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Analyze (javascript)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/app-components/TimePicker/README.md (1)
33-33
: Fix import path and 12-hour format token in the usage example
- The import path likely points to a non-existent module; the component file appears to live under
components/TimePicker.tsx
or is re-exported elsewhere.- The 12-hour example uses the 24-hour token (
HH
) together witha
(AM/PM). For 12-hour formats, usehh
.Run this script to verify the actual export location before applying the change:
#!/bin/bash # Locate TimePicker source and any re-exports fd -t f -e ts -e tsx TimePicker rg -nP --type ts --type tsx -C2 'export\s+\{?\s*TimePicker\s*\}?'Apply this diff to the README:
-import { TimePicker } from 'src/app-components/TimePicker/TimePicker'; +import { TimePicker } from 'src/app-components/TimePicker/components/TimePicker'; @@ - format="HH:mm:ss a" + format="hh:mm:ss a"Also applies to: 44-51
🧹 Nitpick comments (9)
src/app-components/TimePicker/README.md (9)
25-30
: Document supported format tokens where “Format Support” is introducedAdding an explicit token reference reduces confusion and prevents misuse (e.g.,
HH
vshh
).## 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 +- **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 + +### Token reference +- `HH` — hours 00–23 (24-hour) +- `hh` — hours 01–12 (12-hour) +- `mm` — minutes 00–59 +- `ss` — seconds 00–59 +- `a` — period marker (AM/PM) + +Note: When using `a`, pair it with `hh` (not `HH`).
57-65
: Clarify prop semantics for value/onChange/formatMake it explicit that
onChange
emits a string in the activeformat
, and whatvalue
should look like for each mode.-`onChange: (value: string) => void` - Callback when time value changes +`onChange: (value: string) => void` - Called with the time string formatted according to `format` @@ -`value?: string` - Current time value in the specified format -`format?: TimeFormat` - Time format string (default: "HH:mm") +`value?: string` - Current time value as a string matching `format` (e.g., "14:30" for "HH:mm", "02:30 PM" for "hh:mm a") +`format?: TimeFormat` - Time format string controlling display and emitted value (default: "HH:mm")
20-24
: Punctuation/clarity: add serial comma in separators listSmall readability tweak.
-- **Separators**: Type ":", ".", "," or space to advance to next segment +- **Separators**: Type ":", ".", ",", or space to advance to the next segment
16-16
: Grammar: “the next” improves flow-- **Auto-advance**: Automatically moves to next segment +- **Auto-advance**: Automatically moves to the next segment
120-125
: Hyphenation and consistency in Period InputMinor readability fixes.
-- **Case insensitive**: Accepts both upper and lower case +- **Case-insensitive**: Accepts both upper and lower case
137-141
: ARIA phrasing: add article and code formatting-- **ARIA Labels**: Each segment has descriptive aria-label +- **ARIA Labels**: Each segment has a descriptive `aria-label`
110-113
: Terminology: replace “0X” placeholder with “0x”Lowercase “x” is the conventional placeholder for “some digit”.
-- **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 +-- **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 @@ -- **First digit**: 0-5 waits for second digit, 6-9 auto-coerces to 0X +- **First digit**: 0-5 waits for second digit, 6-9 auto-coerces to 0xAlso applies to: 116-119
131-134
: Avoid “race condition” wording in buffer section“Race condition” is overloaded; “stale closure” is more precise for React event timing.
-3. **Race Condition Prevention**: Uses refs to avoid stale closure issues +3. **Stale Closure Prevention**: Uses refs to avoid stale-closure issues
30-51
: Optional: add a quick controlled vs. uncontrolled note under UsageThis helps integrators understand how
value
andonChange
interplay.## Usage @@ ```tsx @@ </>
+Note: The component is controlled. When
value
is provided, the displayed time mirrors that value; user edits are surfaced viaonChange(value)
, and it’s the parent’s responsibility to updatevalue
. Ifvalue
is omitted, the component manages its internal state but still callsonChange
with the formatted string on edits.</blockquote></details> </blockquote></details> <details> <summary>📜 Review details</summary> **Configuration used**: CodeRabbit UI **Review profile**: CHILL **Plan**: Pro **💡 Knowledge Base configuration:** - MCP integration is disabled by default for public repositories - Jira integration is disabled by default for public repositories - Linear integration is disabled by default for public repositories You can enable these sources in your CodeRabbit configuration. <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between ecceadb82cdf67807e482ca634db4d814f2fcaa6 and 16526575eb4fd512b41e83399240579db42a8e0b. </details> <details> <summary>📒 Files selected for processing (1)</summary> * `src/app-components/TimePicker/README.md` (1 hunks) </details> <details> <summary>🧰 Additional context used</summary> <details> <summary>🪛 LanguageTool</summary> <details> <summary>src/app-components/TimePicker/README.md</summary> [grammar] ~16-~16: There might be a mistake here. Context: ...- **Auto-advance**: Automatically moves to next segment when current segment is co... (QB_NEW_EN) --- [grammar] ~16-~16: There might be a mistake here. Context: ...**: Automatically moves to next segment when current segment is complete ### Keyboa... (QB_NEW_EN) --- [grammar] ~23-~23: There might be a mistake here. Context: ... Type ":", ".", "," or space to advance to next segment ### Format Support - **2... (QB_NEW_EN) --- [grammar] ~27-~27: There might be a mistake here. Context: ...*24-hour format**: "HH:mm" or "HH:mm:ss" - **12-hour format**: "hh:mm a" or "hh:mm:ss... (QB_NEW_EN) --- [grammar] ~28-~28: There might be a mistake here. Context: ...: "hh:mm a" or "hh:mm:ss a" (with AM/PM) - **Flexible display**: Configurable time fo... (QB_NEW_EN) --- [grammar] ~29-~29: There might be a mistake here. Context: ...urable time format with optional seconds ## Usage ```tsx import { TimePicker } from... (QB_NEW_EN) --- [grammar] ~82-~82: There might be a mistake here. Context: ...t for hours, minutes, seconds, or period - Implements Chrome-like typing behavior w... (QB_NEW_EN) --- [grammar] ~83-~83: There might be a mistake here. Context: ...e typing behavior with buffer management - Handles keyboard navigation and value co... (QB_NEW_EN) --- [grammar] ~90-~90: There might be a mistake here. Context: ...ercion logic for different segment types - **Buffer Management**: Handles multi-chara... (QB_NEW_EN) --- [grammar] ~91-~91: There might be a mistake here. Context: ...dles multi-character input with timeouts - **Validation**: Ensures values stay within... (QB_NEW_EN) --- [grammar] ~96-~96: There might be a mistake here. Context: ...*: Arrow key navigation between segments - **Value Manipulation**: Increment/decremen... (QB_NEW_EN) --- [grammar] ~97-~97: There might be a mistake here. Context: ...n**: Increment/decrement with arrow keys - **Key Handling**: Special key processing (... (QB_NEW_EN) --- [grammar] ~110-~110: There might be a mistake here. Context: ...for second digit, 3-9 auto-coerces to 0X - **12-hour mode**: First digit 0-1 waits fo... (QB_NEW_EN) --- [grammar] ~111-~111: There might be a mistake here. Context: ...for second digit, 2-9 auto-coerces to 0X - **Second digit**: Validates against first ... (QB_NEW_EN) --- [grammar] ~116-~116: There might be a mistake here. Context: ...ond Input - **First digit**: 0-5 waits for second digit, 6-9 auto-coerces to 0X - ... (QB_NEW_EN) --- [grammar] ~116-~116: There might be a mistake here. Context: ...for second digit, 6-9 auto-coerces to 0X - **Second digit**: Always accepts 0-9 - **O... (QB_NEW_EN) --- [grammar] ~122-~122: There might be a mistake here. Context: ...Input (AM/PM) - **A/a key**: Sets to AM - **P/p key**: Sets to PM - **Case insensiti... (QB_NEW_EN) --- [grammar] ~123-~123: There might be a mistake here. Context: ...**: Sets to AM - **P/p key**: Sets to PM - **Case insensitive**: Accepts both upper a... (QB_NEW_EN) --- [grammar] ~137-~137: There might be a mistake here. Context: ...bility - **ARIA Labels**: Each segment has descriptive aria-label - **Keyboard Nav... (QB_NEW_EN) --- [grammar] ~146-~146: There might be a mistake here. Context: ...*: Various input patterns and edge cases - **Navigation**: Keyboard navigation betwee... (QB_NEW_EN) --- [grammar] ~147-~147: There might be a mistake here. Context: ...**: Keyboard navigation between segments - **Buffer Management**: Race condition prev... (QB_NEW_EN) --- [grammar] ~148-~148: There might be a mistake here. Context: ...ondition prevention and timeout handling - **Format Support**: Different time formats... (QB_NEW_EN) --- [grammar] ~149-~149: There might be a mistake here. Context: ...*: Different time formats and validation - **Accessibility**: Screen reader compatibi... (QB_NEW_EN) </details> </details> </details> <details> <summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)</summary> * GitHub Check: Type-checks, eslint, unit tests and SonarCloud * GitHub Check: Analyze (javascript) * GitHub Check: Install </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
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.
Actionable comments posted: 5
♻️ Duplicate comments (6)
src/app-components/TimePicker/TimePicker.tsx (3)
507-507
: Localize hardcoded labels. [duplicate of prior bot note]Replace Norwegian/English literals with the existing
segmentLabels
so UI respectslabels
and i18n.- <div className={styles.dropdownLabel}>Timer</div> + <div className={styles.dropdownLabel}>{segmentLabels.hours}</div> ... - <div className={styles.dropdownLabel}>Minutter</div> + <div className={styles.dropdownLabel}>{segmentLabels.minutes}</div> ... - <div className={styles.dropdownLabel}>Sekunder</div> + <div className={styles.dropdownLabel}>{segmentLabels.seconds}</div> ... - <div className={styles.dropdownLabel}>AM/PM</div> + <div className={styles.dropdownLabel}>{segmentLabels.period}</div>Also consider localizing the trigger’s
aria-label
“Open time picker”.Also applies to: 570-570, 626-626, 683-683
232-235
: Replace nested ternary with readable conditionals. [duplicate of prior bot note]- case 2: - return includesSeconds ? secondOptions : is12Hour ? [{ value: 'AM' }, { value: 'PM' }] : []; + case 2: { + if (includesSeconds) return secondOptions; + return is12Hour ? [{ value: 'AM' }, { value: 'PM' }] : []; + }
514-528
: Hour validation ternary is hard to parse; extract helper and avoid recomputing constraints per option. [duplicate of prior bot note]Add a small helper and precompute valid hours once:
+// place above the map +const to24h = (optionHour: number, is12Hour: boolean, period?: 'AM' | 'PM') => { + if (!is12Hour) return optionHour; + if (optionHour === 12) return period === 'AM' ? 0 : 12; + return period === 'PM' ? optionHour + 12 : optionHour; +}; +const validHourSet = + constraints.minTime || constraints.maxTime + ? new Set(getSegmentConstraints('hours', timeValue, constraints, format).validValues) + : undefined; ... - const isDisabled = - constraints.minTime || constraints.maxTime - ? !getSegmentConstraints('hours', timeValue, constraints, format).validValues.includes( - is12Hour - ? option.value === 12 - ? timeValue.period === 'AM' - ? 0 - : 12 - : timeValue.period === 'PM' && option.value !== 12 - ? option.value + 12 - : option.value - : option.value, - ) - : false; + const normalized = to24h(option.value, is12Hour, timeValue.period); + const isDisabled = validHourSet ? !validHourSet.has(is12Hour ? option.value : normalized) : false;Note: for 12h,
validValues
are 1–12, hence thehas(is12Hour ? option.value : normalized)
check.src/app-components/TimePicker/TimeSegment/TimeSegment.tsx (3)
65-75
: Effect depends on a changing object; stabilize with useCallback or primitive flag. [duplicate of prior bot note]- const syncExternalChangesWhenNotTyping = () => { - console.log('syncExternalChangesWhenNotTyping called, isTyping:', typingBuffer.isTyping); - if (!typingBuffer.isTyping) { - console.log('syncing external value and resetting to idle'); - syncWithExternalValue(); - typingBuffer.resetToIdleState(); - } - }; - - React.useEffect(syncExternalChangesWhenNotTyping, [value, type, format, syncWithExternalValue, typingBuffer]); + const syncExternalChangesWhenNotTyping = React.useCallback(() => { + if (!typingBuffer.isTyping) { + syncWithExternalValue(); + typingBuffer.resetToIdleState(); + } + }, [typingBuffer.isTyping, typingBuffer.resetToIdleState, syncWithExternalValue]); + + React.useEffect(syncExternalChangesWhenNotTyping, [syncExternalChangesWhenNotTyping, value, type, format]);
133-137
: Use onKeyDown; onKeyPress is deprecated. [duplicate of prior bot note]- onKeyPress={handleCharacterTyping} - onKeyDown={handleSpecialKeys} + onKeyDown={(event) => { + handleSpecialKeys(event); + if (!event.defaultPrevented) { + handleCharacterTyping(event); + } + }}
134-143
: Controlled input pattern. [duplicate of prior bot note]If input is fully keyboard-driven, prefer marking it
readOnly
and remove the no-oponChange
to avoid confusion.- onChange={() => {}} + // Value is controlled via keyboard handlers; prevent native edits + readOnly={true}Then you can drop the separate
readOnly
prop or OR it withtrue
.
🧹 Nitpick comments (6)
src/app-components/TimePicker/TimePicker.tsx (3)
577-583
: Minor perf: don’t recompute constraints inside map loops.Compute
validValues
once per column (hours/minutes/seconds) before mapping and reuse.Also applies to: 634-639
186-188
: Avoid unsafe casts in DOM query.Use generic overload of
querySelectorAll
to get properly typed buttons.- const buttons = container.querySelectorAll('button'); - return (buttons[optionIndex] as HTMLButtonElement) || null; + const buttons = container.querySelectorAll<HTMLButtonElement>('button'); + return buttons[optionIndex] || null;
130-134
: Guard against stale base when composing updates.
updateTime
spreads overtimeValue
from the current render; multiple rapid updates before parent re-renders can clobber each other.- const updateTime = (updates: Partial<TimeValue>) => { - const newTime = { ...timeValue, ...updates }; - onChange(formatTimeValue(newTime, format)); - }; + const updateTime = React.useCallback((updates: Partial<TimeValue>) => { + const base = parseTimeString(value, format); + onChange(formatTimeValue({ ...base, ...updates }, format)); + }, [value, format, onChange]);src/app-components/TimePicker/TimeSegment/TimeSegment.tsx (3)
66-69
: Remove console logging in production paths.Logs clutter the console; gate behind dev checks or remove.
- console.log('syncExternalChangesWhenNotTyping called, isTyping:', typingBuffer.isTyping); ... - console.log('syncing external value and resetting to idle'); ... - console.log('focus'); ... - console.log('blur');Also applies to: 112-115, 118-121
154-155
: Nit: redundant conditional.
maxLength
is 2 for all segments.- maxLength={type === 'period' ? 2 : 2} + maxLength={2}
13-14
: Unused props (min
,max
).They’re not consumed by this component; either use for a11y (e.g.,
aria-valuemin/max
withrole='spinbutton'
) or remove.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/app-components/TimePicker/TimePicker.module.css
(1 hunks)src/app-components/TimePicker/TimePicker.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/TimeSegment.tsx
(1 hunks)src/language/texts/en.ts
(1 hunks)src/language/texts/nb.ts
(1 hunks)src/language/texts/nn.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/app-components/TimePicker/TimePicker.module.css
- src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx
- src/language/texts/nb.ts
- src/language/texts/en.ts
- src/language/texts/nn.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/app-components/TimePicker/TimeSegment/TimeSegment.tsx
src/app-components/TimePicker/TimePicker.tsx
🧬 Code graph analysis (2)
src/app-components/TimePicker/TimeSegment/TimeSegment.tsx (5)
src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
SegmentType
(4-4)src/app-components/TimePicker/TimePicker.tsx (1)
TimeFormat
(25-25)src/app-components/TimePicker/TimeSegment/hooks/useSegmentDisplay.ts (1)
useSegmentDisplay
(7-28)src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (1)
useSegmentInputHandlers
(27-123)src/app-components/TimePicker/TimeSegment/hooks/useTypingBuffer.ts (1)
useTypingBuffer
(11-88)
src/app-components/TimePicker/TimePicker.tsx (7)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (4)
parseTimeString
(21-61)TimeConstraints
(10-13)TimeValue
(3-8)getSegmentConstraints
(87-194)src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.ts (3)
DropdownFocusState
(1-5)NavigationAction
(7-13)calculateNextFocusState
(23-80)src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
SegmentType
(4-4)src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
formatTimeValue
(5-26)src/app-components/TimePicker/utils/formatDisplayHour/formatDisplayHour.ts (1)
formatDisplayHour
(7-22)src/app-components/TimePicker/utils/generateTimeOptions/generateTimeOptions.ts (3)
generateHourOptions
(11-23)generateMinuteOptions
(30-40)generateSecondOptions
(47-57)src/app-components/TimePicker/TimeSegment/TimeSegment.tsx (1)
TimeSegment
(29-159)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Analyze (javascript)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
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.
Actionable comments posted: 3
♻️ Duplicate comments (2)
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (1)
108-114
: Previous review resolved: delete/backspace now clears and commits correctlyClear handler now updates display and commits a cleared value path.
src/app-components/TimePicker/utils/segmentTyping.ts (1)
200-209
: Previous review resolved: period input now respects current state
processPeriodInput
now receives the actual current period instead of hardcoded 'AM'.
🧹 Nitpick comments (5)
src/app-components/TimePicker/utils/normalizeHour.ts (1)
10-20
: Add defensive range checks (fail fast on invalid inputs)Guard against out‑of‑range values to avoid silent bad states (e.g., 0 or 13 in 12h mode; 24+ in 24h).
Apply this diff:
export function normalizeHour(optionValue: number, is12Hour: boolean, period: 'AM' | 'PM'): number { - if (!is12Hour) { - return optionValue; - } + if (!is12Hour) { + if (optionValue < 0 || optionValue > 23) { + throw new RangeError('normalizeHour: optionValue must be 0..23 in 24-hour mode'); + } + return optionValue; + } + + if (optionValue < 1 || optionValue > 12) { + throw new RangeError('normalizeHour: optionValue must be 1..12 in 12-hour mode'); + } if (optionValue === 12) { return period === 'AM' ? 0 : 12; } return period === 'PM' ? optionValue + 12 : optionValue; }src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (2)
51-57
: Prefer Number.isNaN for stricter NaN checkMinor robustness/readability.
- const valueIsEmpty = - currentValue === null || currentValue === '' || (typeof currentValue === 'number' && isNaN(currentValue)); + const valueIsEmpty = + currentValue === null || currentValue === '' || (typeof currentValue === 'number' && Number.isNaN(currentValue));
26-43
: Optional: memoize handlers to stabilize identitiesWrap returned handlers with useCallback to reduce re-renders in children.
Also applies to: 87-106, 116-122
src/app-components/TimePicker/utils/segmentTyping.ts (2)
129-135
: Normalize period buffer to uppercase and validatePrevents lowercase or mixed-case leakage.
if (segmentType === 'period') { + const up = buffer.toUpperCase(); return { - displayValue: buffer, - actualValue: buffer, - isComplete: buffer === 'AM' || buffer === 'PM', + displayValue: up, + actualValue: up, + isComplete: up === 'AM' || up === 'PM', }; }
120-153
: Consider using is12Hour to refine hour completenessYou pass
_is12Hour
but don’t use it; could fine-tuneisComplete
for hours. Low priority.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/app-components/TimePicker/TimePicker.responsive.test.tsx
(1 hunks)src/app-components/TimePicker/TimePicker.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/TimeSegment.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
(1 hunks)src/app-components/TimePicker/TimeSegment/hooks/useTypingBuffer.ts
(1 hunks)src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.test.ts
(1 hunks)src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.test.ts
(1 hunks)src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
(1 hunks)src/app-components/TimePicker/utils/normalizeHour.ts
(1 hunks)src/app-components/TimePicker/utils/segmentTyping.ts
(1 hunks)src/app-components/TimePicker/utils/timeConstraintUtils.test.ts
(1 hunks)src/app-components/TimePicker/utils/timeFormatUtils.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
- src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.test.ts
- src/app-components/TimePicker/TimeSegment/hooks/useTypingBuffer.ts
- src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.test.ts
- src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx
- src/app-components/TimePicker/utils/timeConstraintUtils.test.ts
- src/app-components/TimePicker/TimeSegment/TimeSegment.tsx
- src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
- src/app-components/TimePicker/TimePicker.tsx
- src/app-components/TimePicker/utils/timeFormatUtils.ts
- src/app-components/TimePicker/TimePicker.responsive.test.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/app-components/TimePicker/utils/normalizeHour.ts
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
src/app-components/TimePicker/utils/segmentTyping.ts
🧬 Code graph analysis (2)
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (3)
src/app-components/TimePicker/utils/keyboardNavigation.ts (4)
SegmentType
(4-4)handleValueIncrement
(74-110)handleValueDecrement
(112-148)handleSegmentKeyDown
(14-58)src/app-components/TimePicker/TimePicker.tsx (1)
TimeFormat
(26-26)src/app-components/TimePicker/utils/segmentTyping.ts (4)
clearSegment
(158-161)commitSegmentValue
(166-174)handleSegmentCharacterInput
(179-234)processSegmentBuffer
(120-153)
src/app-components/TimePicker/utils/segmentTyping.ts (2)
src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
SegmentType
(4-4)src/app-components/TimePicker/TimePicker.tsx (1)
TimeFormat
(26-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
- GitHub Check: Install
- GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
src/app-components/TimePicker/utils/normalizeHour.ts (1)
10-20
: 12h→24h mapping looks correctAM 12→0, PM 1–11→13–23, and passthrough for 24h are correct.
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
Show resolved
Hide resolved
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
Show resolved
Hide resolved
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.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (1)
26-125
: Missing React optimization for function stabilityThe hook exposes functions that will be recreated on every render, potentially causing unnecessary re-renders of child components. React's useCallback requires dependency arrays that include "all reactive values referenced inside of the fn code" for proper memoization.
Apply this diff to optimize function stability with useCallback:
+import { useCallback } from 'react'; import type React from 'react'; export function useSegmentInputHandlers({ segmentType, timeFormat, currentValue, onValueChange, onNavigate, onUpdateDisplay, }: SegmentInputConfig) { - function incrementCurrentValue() { + const incrementCurrentValue = useCallback(() => { const newValue = handleValueIncrement(currentValue, segmentType, timeFormat); onValueChange(newValue); - } + }, [currentValue, segmentType, timeFormat, onValueChange]); - function decrementCurrentValue() { + const decrementCurrentValue = useCallback(() => { const newValue = handleValueDecrement(currentValue, segmentType, timeFormat); onValueChange(newValue); - } + }, [currentValue, segmentType, timeFormat, onValueChange]); - function clearCurrentValueAndDisplay() { + const clearCurrentValueAndDisplay = useCallback(() => { const clearedSegment = clearSegment(); onUpdateDisplay(clearedSegment.displayValue); if (segmentType === 'period') { onValueChange(null); } else { const committedValue = commitSegmentValue(clearedSegment.actualValue, segmentType); onValueChange(committedValue); } - } + }, [segmentType, onUpdateDisplay, onValueChange]); - function fillEmptyTimeSegmentWithZero() { + const fillEmptyTimeSegmentWithZero = useCallback(() => { const valueIsEmpty = currentValue === null || currentValue === '' || (typeof currentValue === 'number' && isNaN(currentValue)); if (valueIsEmpty && (segmentType === 'minutes' || segmentType === 'seconds')) { onValueChange(0); } - } + }, [currentValue, segmentType, onValueChange]); - function processCharacterInput(character: string, currentBuffer: string) { + const processCharacterInput = useCallback((character: string, currentBuffer: string) => { const inputResult = handleSegmentCharacterInput(character, segmentType, currentBuffer, timeFormat); const bufferResult = processSegmentBuffer(inputResult.newBuffer, segmentType, timeFormat.includes('a')); onUpdateDisplay(bufferResult.displayValue); return { newBuffer: inputResult.newBuffer, shouldNavigateRight: inputResult.shouldNavigate || inputResult.shouldAdvance, shouldCommitImmediately: inputResult.shouldAdvance, processedValue: bufferResult.actualValue, }; - } + }, [segmentType, timeFormat, onUpdateDisplay]); - function commitBufferValue(bufferValue: string) { + const commitBufferValue = useCallback((bufferValue: string) => { if (segmentType === 'period') { onValueChange(bufferValue); return; } const processed = processSegmentBuffer(bufferValue, segmentType, timeFormat.includes('a')); const committedValue = commitSegmentValue(processed.actualValue, segmentType); onValueChange(committedValue); - } + }, [segmentType, timeFormat, onValueChange]); - function handleArrowKeyNavigation(event: React.KeyboardEvent<HTMLInputElement>) { + const handleArrowKeyNavigation = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => { const result = handleSegmentKeyDown(event); if (result.shouldNavigate && result.direction) { onNavigate(result.direction); return true; } if (result.shouldIncrement) { incrementCurrentValue(); return true; } if (result.shouldDecrement) { decrementCurrentValue(); return true; } return false; - } + }, [onNavigate, incrementCurrentValue, decrementCurrentValue]); - function handleDeleteOrBackspace() { + const handleDeleteOrBackspace = useCallback(() => { clearCurrentValueAndDisplay(); - } + }, [clearCurrentValueAndDisplay]); - function fillEmptyMinutesOrSecondsWithZero() { + const fillEmptyMinutesOrSecondsWithZero = useCallback(() => { fillEmptyTimeSegmentWithZero(); - } + }, [fillEmptyTimeSegmentWithZero]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
(1 hunks)src/app-components/TimePicker/TimeSegment/hooks/useTimeout.ts
(1 hunks)src/app-components/TimePicker/utils/segmentTyping.ts
(1 hunks)src/app-components/TimePicker/utils/timeConstraintUtils.ts
(1 hunks)src/app-components/TimePicker/utils/timeFormatUtils.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx
- src/app-components/TimePicker/utils/timeConstraintUtils.ts
- src/app-components/TimePicker/TimeSegment/hooks/useTimeout.ts
- src/app-components/TimePicker/utils/timeFormatUtils.ts
- src/app-components/TimePicker/utils/segmentTyping.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
🧬 Code graph analysis (1)
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (3)
src/app-components/TimePicker/utils/keyboardNavigation.ts (4)
SegmentType
(4-4)handleValueIncrement
(74-110)handleValueDecrement
(112-148)handleSegmentKeyDown
(14-58)src/app-components/TimePicker/TimePicker.tsx (1)
TimeFormat
(26-26)src/app-components/TimePicker/utils/segmentTyping.ts (4)
clearSegment
(158-161)commitSegmentValue
(166-179)handleSegmentCharacterInput
(183-238)processSegmentBuffer
(120-153)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
- GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (1)
76-87
: Inconsistent commit behavior prevents fallback to default valuesThe condition
if (processed.actualValue !== null)
prevents committingnull
values, which blocks the intended fallback behavior (e.g., minutes/seconds defaulting to 00). This contradicts previous review feedback about always committing processed buffers.Based on previous patterns and the dependency management requirements, apply this diff to ensure consistent commit behavior:
const processed = processSegmentBuffer(bufferValue, segmentType, timeFormat.includes('a')); - if (processed.actualValue !== null) { - const committedValue = commitSegmentValue(processed.actualValue, segmentType); - onValueChange(committedValue); - } + const committedValue = commitSegmentValue(processed.actualValue, segmentType); + onValueChange(committedValue);
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
Show resolved
Hide resolved
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.
Actionable comments posted: 7
🧹 Nitpick comments (8)
src/layout/TimePicker/useTimePickerValidation.ts (1)
69-70
: Remove dead/commented codeDrop the commented
timeToMinutes
function.-// const timeToMinutes = (time: { hours: number; minutes: number }): number => time.hours * 60 + time.minutes;
src/layout/TimePicker/TimePickerComponent.tsx (1)
61-71
: Consider wiring autocomplete and aria-describedbyIf the config’s
autocomplete
is present, pass it through. Also ensure the control references the label/description IDs for SR context.Do you want me to thread
autocomplete
andaria-describedby
from the Label helpers intoTimePickerControl
?src/app-components/TimePicker/utils/timeConstraintUtils.test.ts (1)
9-13
: Use the exported SegmentConstraints type instead of redefining itImporting the shared type avoids drift.
-import interface SegmentConstraints { - min: number; - max: number; - validValues: number[]; -} +import type { SegmentConstraints } from 'src/app-components/TimePicker/types';src/layout/TimePicker/index.tsx (2)
67-82
: Tighten validation hook + remove dead code.
_component
is unused.useLayoutLookups()
called twice; keep one.- Simplify error extraction.
Apply:
useDataModelBindingValidation(baseComponentId: string, bindings: IDataModelBindings<'TimePicker'>): string[] { - const lookupBinding = DataModels.useLookupBinding(); - const layoutLookups = useLayoutLookups(); - const _component = useLayoutLookups().getComponent(baseComponentId, 'TimePicker'); - const validation = validateDataModelBindingsAny( + const lookupBinding = DataModels.useLookupBinding(); + const layoutLookups = useLayoutLookups(); + const validation = validateDataModelBindingsAny( baseComponentId, bindings, lookupBinding, layoutLookups, 'simpleBinding', ['string'], ); - const [errors] = [validation[0] ?? []]; - - return errors; + return validation[0] ?? []; }
39-47
: Avoid name shadowing with hook import.This class also defines
useDisplayData
; the imported hook of the same name is used here. Alias the import to prevent confusion.Apply:
-import { useDisplayData } from 'src/features/displayData/useDisplayData'; +import { useDisplayData as useNodeDisplayData } from 'src/features/displayData/useDisplayData'; @@ - renderSummary(props: SummaryRendererProps): JSX.Element | null { - const displayData = useDisplayData(props.targetBaseComponentId); + renderSummary(props: SummaryRendererProps): JSX.Element | null { + const displayData = useNodeDisplayData(props.targetBaseComponentId);src/app-components/TimePicker/utils/segmentTyping.ts (2)
154-167
: Type/logic mismatch: null branch is unreachable.
commitSegmentValue
checksvalue === null
but the function’s type isnumber | string
, and call sites guard against null. Either acceptnull
or remove the branch.Apply one of:
Option A (accept null):
-export const commitSegmentValue = (value: number | string, segmentType: SegmentType): number | string => { - if (value === null) { +export const commitSegmentValue = ( + value: number | string | null, + segmentType: SegmentType, +): number | string => { + if (value == null) { if (segmentType === 'minutes' || segmentType === 'seconds') { return 0; // Fill empty minutes/seconds with 00 } if (segmentType === 'hours') { return 0; // Default for hours too } if (segmentType === 'period') { return 'AM'; // Safe default for period } } return value; };Option B (keep type; delete dead null branch).
108-118
: Unused parameter_is12Hour
.Drop it or use it; otherwise it’s noise.
Apply:
-export const processSegmentBuffer = (buffer: string, segmentType: SegmentType, _is12Hour: boolean): SegmentBuffer => { +export const processSegmentBuffer = (buffer: string, segmentType: SegmentType): SegmentBuffer => {Also update callers in
useSegmentInputHandlers.ts
.src/app-components/TimePicker/types.ts (1)
46-62
: Unused TimeSegmentProps.min/max — wire to ARIA or removeTimeSegmentProps declares min/max but TimeSegment (src/app-components/TimePicker/TimeSegment/TimeSegment.tsx) never destructures or forwards them. Either add min/max to the component props and pass them to the Textfield as aria-valuemin/aria-valuemax (and use them for validation/keyboard behavior), or remove them from the interface to avoid confusion.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (23)
src/app-components/TimePicker/TimePicker.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/TimeSegment.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/hooks/useSegmentDisplay.ts
(1 hunks)src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
(1 hunks)src/app-components/TimePicker/TimeSegment/hooks/useTypingBuffer.ts
(1 hunks)src/app-components/TimePicker/types.ts
(1 hunks)src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.test.ts
(1 hunks)src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.ts
(1 hunks)src/app-components/TimePicker/utils/generateTimeOptions/generateTimeOptions.ts
(1 hunks)src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.test.ts
(1 hunks)src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
(1 hunks)src/app-components/TimePicker/utils/keyboardNavigation.ts
(1 hunks)src/app-components/TimePicker/utils/segmentTyping.ts
(1 hunks)src/app-components/TimePicker/utils/timeConstraintUtils.test.ts
(1 hunks)src/app-components/TimePicker/utils/timeConstraintUtils.ts
(1 hunks)src/app-components/TimePicker/utils/timeFormatUtils.test.ts
(1 hunks)src/app-components/TimePicker/utils/timeFormatUtils.ts
(1 hunks)src/layout/Datepicker/config.ts
(0 hunks)src/layout/TimePicker/TimePickerComponent.tsx
(1 hunks)src/layout/TimePicker/config.ts
(1 hunks)src/layout/TimePicker/index.tsx
(1 hunks)src/layout/TimePicker/useTimePickerValidation.ts
(1 hunks)
💤 Files with no reviewable changes (1)
- src/layout/Datepicker/config.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.test.ts
- src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx
- src/app-components/TimePicker/utils/generateTimeOptions/generateTimeOptions.ts
- src/app-components/TimePicker/TimeSegment/hooks/useTypingBuffer.ts
- src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.ts
- src/app-components/TimePicker/utils/timeFormatUtils.test.ts
- src/app-components/TimePicker/TimeSegment/hooks/useSegmentDisplay.ts
- src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/app-components/TimePicker/utils/timeFormatUtils.ts
src/layout/TimePicker/config.ts
src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
src/layout/TimePicker/index.tsx
src/app-components/TimePicker/utils/timeConstraintUtils.test.ts
src/layout/TimePicker/useTimePickerValidation.ts
src/app-components/TimePicker/utils/segmentTyping.ts
src/app-components/TimePicker/types.ts
src/app-components/TimePicker/utils/timeConstraintUtils.ts
src/layout/TimePicker/TimePickerComponent.tsx
src/app-components/TimePicker/TimeSegment/TimeSegment.tsx
src/app-components/TimePicker/utils/keyboardNavigation.ts
src/app-components/TimePicker/TimePicker.tsx
src/layout/*/{config.ts,Component.tsx,index.tsx,config.generated.ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Layout components must follow the standardized structure:
config.ts
,Component.tsx
,index.tsx
, and include generated types inconfig.generated.ts
Files:
src/layout/TimePicker/config.ts
src/layout/TimePicker/index.tsx
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
In tests, use
renderWithProviders
fromsrc/test/renderWithProviders.tsx
to supply required form layout context
Files:
src/app-components/TimePicker/utils/timeConstraintUtils.test.ts
🧠 Learnings (2)
📚 Learning: 2025-08-22T13:53:28.252Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.252Z
Learning: Applies to src/layout/*/{config.ts,Component.tsx,index.tsx,config.generated.ts} : Layout components must follow the standardized structure: `config.ts`, `Component.tsx`, `index.tsx`, and include generated types in `config.generated.ts`
Applied to files:
src/layout/TimePicker/config.ts
📚 Learning: 2025-08-22T13:53:28.252Z
Learnt from: CR
PR: Altinn/app-frontend-react#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-22T13:53:28.252Z
Learning: Applies to **/*.{ts,tsx} : Avoid using `any` and unnecessary type casts (`as Type`) in TypeScript; prefer precise typings and refactor existing casts/anys
Applied to files:
src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
🧬 Code graph analysis (14)
src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
src/app-components/TimePicker/types.ts (3)
TimeValue
(9-14)TimeFormat
(2-2)SegmentType
(5-5)
src/layout/TimePicker/config.ts (2)
src/layout/Datepicker/config.ts (1)
Config
(5-75)src/codegen/CG.ts (1)
CG
(25-57)
src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts (1)
src/app-components/TimePicker/types.ts (5)
TimeValue
(9-14)SegmentChangeResult
(104-106)NumericSegmentType
(6-6)SegmentConstraints
(22-26)SegmentType
(5-5)
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (3)
src/app-components/TimePicker/types.ts (1)
SegmentInputConfig
(109-116)src/app-components/TimePicker/utils/keyboardNavigation.ts (3)
handleValueIncrement
(68-104)handleValueDecrement
(106-142)handleSegmentKeyDown
(8-52)src/app-components/TimePicker/utils/segmentTyping.ts (4)
clearSegment
(146-149)commitSegmentValue
(154-167)handleSegmentCharacterInput
(171-226)processSegmentBuffer
(108-141)
src/layout/TimePicker/index.tsx (12)
src/layout/index.ts (4)
ValidateComponent
(68-70)ValidationFilter
(86-88)PropsFromGenericComponent
(28-32)ValidationFilterFunction
(80-84)src/layout/TimePicker/TimePickerComponent.tsx (1)
TimePickerComponent
(13-76)src/utils/layout/useNodeItem.ts (1)
useNodeFormDataWhenType
(97-103)src/layout/LayoutComponent.tsx (2)
SummaryRendererProps
(174-179)ExprResolver
(41-53)src/layout/Summary/SummaryItemSimple.tsx (1)
SummaryItemSimple
(14-35)src/layout/TimePicker/TimePickerSummary.tsx (1)
TimePickerSummary
(13-43)src/features/validation/index.ts (2)
ComponentValidation
(151-153)BaseValidation
(121-127)src/layout/TimePicker/useTimePickerValidation.ts (1)
useTimePickerValidation
(110-160)src/layout/layout.ts (1)
IDataModelBindings
(61-64)src/features/datamodel/DataModelsProvider.tsx (1)
DataModels
(393-434)src/features/form/layout/LayoutsContext.tsx (1)
useLayoutLookups
(116-116)src/utils/layout/generator/validation/hooks.ts (1)
validateDataModelBindingsAny
(10-56)
src/app-components/TimePicker/utils/timeConstraintUtils.test.ts (2)
src/app-components/TimePicker/types.ts (2)
SegmentConstraints
(22-26)TimeValue
(9-14)src/app-components/TimePicker/utils/timeConstraintUtils.ts (4)
parseTimeString
(3-43)isTimeInRange
(45-67)getSegmentConstraints
(69-176)getNextValidValue
(178-207)
src/layout/TimePicker/useTimePickerValidation.ts (6)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
parseTimeString
(3-43)src/app-components/TimePicker/types.ts (1)
TimeFormat
(2-2)src/features/validation/index.ts (1)
ComponentValidation
(151-153)src/utils/layout/hooks.ts (1)
useDataModelBindingsFor
(102-112)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/features/formData/FormDataWrite.tsx (1)
FD
(685-1098)
src/app-components/TimePicker/utils/segmentTyping.ts (1)
src/app-components/TimePicker/types.ts (4)
SegmentTypingResult
(85-88)SegmentType
(5-5)SegmentBuffer
(90-94)TimeFormat
(2-2)
src/app-components/TimePicker/types.ts (1)
src/codegen/CodeGenerator.ts (1)
Extract
(37-37)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
src/app-components/TimePicker/types.ts (4)
TimeFormat
(2-2)TimeValue
(9-14)TimeConstraints
(17-20)SegmentConstraints
(22-26)
src/layout/TimePicker/TimePickerComponent.tsx (7)
src/layout/index.ts (1)
PropsFromGenericComponent
(28-32)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/features/formData/useDataModelBindings.ts (1)
useDataModelBindings
(42-57)src/features/language/useLanguage.ts (1)
useLanguage
(90-93)src/utils/layout/useLabel.tsx (1)
useLabel
(13-72)src/layout/ComponentStructureWrapper.tsx (1)
ComponentStructureWrapper
(20-48)src/app-components/Flex/Flex.tsx (1)
Flex
(25-84)
src/app-components/TimePicker/TimeSegment/TimeSegment.tsx (4)
src/app-components/TimePicker/types.ts (1)
TimeSegmentProps
(46-62)src/app-components/TimePicker/TimeSegment/hooks/useSegmentDisplay.ts (1)
useSegmentDisplay
(6-27)src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (1)
useSegmentInputHandlers
(16-115)src/app-components/TimePicker/TimeSegment/hooks/useTypingBuffer.ts (1)
useTypingBuffer
(6-75)
src/app-components/TimePicker/utils/keyboardNavigation.ts (1)
src/app-components/TimePicker/types.ts (4)
SegmentNavigationResult
(96-102)SegmentType
(5-5)TimeFormat
(2-2)SegmentConstraints
(22-26)
src/app-components/TimePicker/TimePicker.tsx (9)
src/app-components/TimePicker/types.ts (6)
TimePickerProps
(29-44)DropdownFocusState
(65-69)SegmentType
(5-5)TimeConstraints
(17-20)TimeValue
(9-14)NavigationAction
(71-77)src/app-components/TimePicker/utils/timeConstraintUtils.ts (2)
parseTimeString
(3-43)getSegmentConstraints
(69-176)src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
formatTimeValue
(3-15)src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts (1)
handleSegmentValueChange
(111-130)src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.ts (1)
calculateNextFocusState
(11-68)src/app-components/TimePicker/utils/formatDisplayHour/formatDisplayHour.ts (1)
formatDisplayHour
(7-22)src/app-components/TimePicker/utils/generateTimeOptions/generateTimeOptions.ts (3)
generateHourOptions
(8-20)generateMinuteOptions
(44-44)generateSecondOptions
(27-37)src/app-components/TimePicker/TimeSegment/TimeSegment.tsx (1)
TimeSegment
(10-135)src/app-components/TimePicker/utils/normalizeHour.ts (1)
normalizeHour
(10-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install
- GitHub Check: Analyze (javascript)
🔇 Additional comments (19)
src/layout/TimePicker/useTimePickerValidation.ts (1)
79-105
: Confirm timezone semantics when timeStamp=true (ISO parsing returns local time)
parseISO()
+getHours()
yields local time. If ISO values are UTC, displayed times may be offset for users in other timezones. Verify intended behavior.Would you like me to add a toggleable option (e.g.,
timeStampTimezone: 'local' | 'utc'
) or normalize usinggetUTCHours()
?src/layout/TimePicker/config.ts (2)
5-57
: Missing generated config types for the new layout component
config.generated.ts
appears absent for TimePicker. CI/codegen may rely on it.Run:
#!/bin/bash fd --full-path 'src/layout/TimePicker/config.generated.ts' -0 | xargs -0 -r ls -lIf missing, run the repo’s codegen and commit the generated file.
5-57
: IstimeStamp
intended as a configurable prop?Validation code reads
timeStamp
from the component, but it’s not defined here. Either drop it from validation or add it to the config.I can add a boolean
timeStamp
property with docs if desired.src/app-components/TimePicker/TimePicker.tsx (3)
604-605
: Localize dropdown column labels (seconds, AM/PM)Hardcoded labels bypass i18n. Use
segmentLabels
.- <div className={styles.dropdownLabel}>Sekunder</div> + <div className={styles.dropdownLabel}>{segmentLabels.seconds}</div> @@ - <div className={styles.dropdownLabel}>AM/PM</div> + <div className={styles.dropdownLabel}>{segmentLabels.amPm}</div>Also applies to: 661-661
455-455
: Remove aria-modal from non-modal Popover
aria-modal
is only valid on modal dialogs. This Popover is a dropdown; drop the attribute (and prefer listbox/menu semantics if needed).- aria-modal='true'
670-697
: Disable AM/PM choices that would violate min/maxPeriod options can lead to out-of-range times. Disable them when invalid.
- {['AM', 'PM'].map((period, optionIndex) => { - const isSelected = timeValue.period === period; + {(['AM', 'PM'] as const).map((period, optionIndex) => { + const hour12 = displayHours; // 1-12 + const candidate24 = + hour12 === 12 ? (period === 'AM' ? 0 : 12) : period === 'PM' ? hour12 + 12 : hour12; + const candidate: TimeValue = { ...timeValue, hours: candidate24, period }; + const hoursOk = getSegmentConstraints('hours', candidate, constraints, format).validValues.includes( + is12Hour ? formatDisplayHour(candidate24, true) : candidate24, + ); + const minutesOk = getSegmentConstraints('minutes', candidate, constraints, format).validValues.includes( + timeValue.minutes, + ); + const secondsOk = !includesSeconds + ? true + : getSegmentConstraints('seconds', candidate, constraints, format).validValues.includes( + timeValue.seconds, + ); + const isDisabled = !(hoursOk && minutesOk && secondsOk); + const isSelected = timeValue.period === period; @@ - <button + <button key={period} type='button' - className={`${styles.dropdownOption} ${ - isSelected ? styles.dropdownOptionSelected : '' - } ${isFocused ? styles.dropdownOptionFocused : ''}`} - onClick={() => { - handleDropdownPeriodChange(period as 'AM' | 'PM'); + className={`${styles.dropdownOption} ${isSelected ? styles.dropdownOptionSelected : ''} ${ + isFocused ? styles.dropdownOptionFocused : '' + } ${isDisabled ? styles.dropdownOptionDisabled : ''}`} + disabled={isDisabled} + onClick={() => { + if (!isDisabled) { + handleDropdownPeriodChange(period as 'AM' | 'PM'); + } setDropdownFocus({ column: columnIndex, option: optionIndex, isActive: true, }); }}src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
45-67
: LGTM — seconds-based comparisons and unified parsingRange check logic is sound and aligns with the parser and segment constraints.
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (2)
34-41
: Clearing a segment does not commit the cleared valueThis leaves the model out of sync with the display. Always commit the cleared value via
commitSegmentValue
.function clearCurrentValueAndDisplay() { const clearedSegment = clearSegment(); onUpdateDisplay(clearedSegment.displayValue); - if (clearedSegment.actualValue && segmentType !== 'period') { - const committedValue = commitSegmentValue(clearedSegment.actualValue, segmentType); - onValueChange(committedValue); - } + const committedValue = commitSegmentValue(clearedSegment.actualValue as unknown as number | string, segmentType); + onValueChange(committedValue); }
66-77
: On blur/commit, minutes/seconds should fall back to 00The null guard prevents intended fallback. Always commit the processed value (the commit helper applies defaults).
function commitBufferValue(bufferValue: string) { if (segmentType === 'period') { onValueChange(bufferValue); return; } const processed = processSegmentBuffer(bufferValue, segmentType, timeFormat.includes('a')); - if (processed.actualValue !== null) { - const committedValue = commitSegmentValue(processed.actualValue, segmentType); - onValueChange(committedValue); - } + const committedValue = commitSegmentValue(processed.actualValue as unknown as number | string, segmentType); + onValueChange(committedValue); }src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
11-14
: Good: AM/PM derived from hours (no stale state).Using
time.hours >= 12 ? 'PM' : 'AM'
avoids desync withperiod
. Looks solid.src/app-components/TimePicker/TimeSegment/TimeSegment.tsx (4)
109-133
: Make input explicitly read‑only to avoid React controlled‑input warnings.Component sets
value
but has noonChange
. EnsurereadOnly
is true at the DOM level; keyboard handling still works viaonKeyDown
.Apply:
- readOnly={readOnly} + readOnly={true}If you must reflect layout read-only, keep
disabled={readOnly || disabled}
.
12-26
: Pass through autoFocus prop.
autoFocus
exists in props but isn’t forwarded to the input.Apply:
className, + autoFocus, }, @@ - className={className} + className={className} + autoFocus={!!autoFocus}Also applies to: 109-133
114-114
: Good: Switched to onKeyDown (onKeyPress is deprecated).Event handling order is correct.
45-53
: useEffect depends on an unstable object; effect re-runs every render.
typingBuffer
is a new object per render; include stable fields/callbacks instead, or memoize the handler.Apply:
- const syncExternalChangesWhenNotTyping = () => { + const syncExternalChangesWhenNotTyping = React.useCallback(() => { if (!typingBuffer.isTyping) { syncWithExternalValue(); typingBuffer.resetToIdleState(); } - }; + }, [typingBuffer.isTyping, typingBuffer.resetToIdleState, syncWithExternalValue]); - - React.useEffect(syncExternalChangesWhenNotTyping, [value, type, format, syncWithExternalValue, typingBuffer]); + React.useEffect(syncExternalChangesWhenNotTyping, [value, type, format, syncExternalChangesWhenNotTyping]);src/app-components/TimePicker/utils/segmentTyping.ts (1)
6-55
: Hour/minute coercion behavior looks consistent and bounded.Input shaping and clamping for 12/24h and 0–59 are sound.
src/app-components/TimePicker/utils/keyboardNavigation.ts (2)
68-104
: Increment logic is correct and wraps as expected.AM/PM toggle and 12h/24h hour wrapping look good. Minute/second wrap to 0 at 59.
8-52
: Key mapping is minimal and testable.Accepting a slim
{ key, preventDefault }
keeps this utility decoupled from React events.src/app-components/TimePicker/types.ts (1)
1-44
: Types are clear and avoidany
.API surface for TimePicker is well‑structured and consistent with usage across the module.
Also applies to: 64-123
src/layout/TimePicker/index.tsx (1)
57-65
: 'validation_errors.pattern' is correct It matches the translation files and is consistently used for schema validation filters.
src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
Outdated
Show resolved
Hide resolved
src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
Show resolved
Hide resolved
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.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/layout/TimePicker/useTimePickerValidation.ts (2)
74-75
: Minor: simplify destructuring
useItemWhenType
never returns undefined; the fallback object is unnecessary.- const { minTime, maxTime, format = 'HH:mm' } = component || {}; + const { minTime, maxTime, format = 'HH:mm' } = component;
93-106
: Guard against misconfigured constraints (minTime > maxTime)If both constraints are present but inverted, skip checks (and optionally log) to avoid false errors.
if (!parsedTime) { validations.push({ message: { key: 'time_picker.invalid_time_message', params: [format] }, severity: 'error', source: FrontendValidationSource.Component, category: ValidationMask.Component, }); return validations; } + // Defensive: ignore inverted constraints to prevent spurious errors + const minParsedForBounds = minTime ? parseTimeString(minTime, format) : null; + const maxParsedForBounds = maxTime ? parseTimeString(maxTime, format) : null; + if ( + minParsedForBounds && + maxParsedForBounds && + timeToSeconds(minParsedForBounds) > timeToSeconds(maxParsedForBounds) + ) { + // Optionally add dev-only logging here + return validations; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/layout/TimePicker/useTimePickerValidation.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/layout/TimePicker/useTimePickerValidation.ts
🧬 Code graph analysis (1)
src/layout/TimePicker/useTimePickerValidation.ts (6)
src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
parseTimeString
(3-43)src/app-components/TimePicker/types.ts (1)
TimeFormat
(2-2)src/features/validation/index.ts (1)
ComponentValidation
(151-153)src/utils/layout/hooks.ts (1)
useDataModelBindingsFor
(102-112)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/features/formData/FormDataWrite.tsx (1)
FD
(685-1098)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Analyze (javascript)
- GitHub Check: Install
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
🔇 Additional comments (4)
src/layout/TimePicker/useTimePickerValidation.ts (4)
1-6
: Import centralized time parser; drop local copyPrefer a single source of truth to avoid drift. Import a strict parser from the shared utils (see next comment) and remove the local implementation.
Apply:
import { FD } from 'src/features/formData/FormDataWrite'; import { type ComponentValidation, FrontendValidationSource, ValidationMask } from 'src/features/validation'; import { useDataModelBindingsFor } from 'src/utils/layout/hooks'; import { useItemWhenType } from 'src/utils/layout/useNodeItem'; import type { TimeFormat } from 'src/app-components/TimePicker/types'; +// Reuse centralized, strict parsing +import { parseTimeStringStrict as parseTimeString } from 'src/app-components/TimePicker/utils/timeConstraintUtils';Companion change in utils (separate file) to host the strict parser:
diff --git a/src/app-components/TimePicker/utils/timeConstraintUtils.ts b/src/app-components/TimePicker/utils/timeConstraintUtils.ts @@ +export const parseTimeStringStrict = ( + timeStr: string, + format: TimeFormat, +): { hours: number; minutes: number; seconds?: number } | null => { + if (!timeStr) return null; + const is12Hour = format.includes('a'); + const includesSeconds = format.includes('ss'); + const cleanTime = timeStr.trim(); + const timeRegex = is12Hour + ? includesSeconds + ? /^(\d{1,2}):(\d{2}):(\d{2})\s*(AM|PM)$/i + : /^(\d{1,2}):(\d{2})\s*(AM|PM)$/i + : includesSeconds + ? /^(\d{1,2}):(\d{2}):(\d{2})$/ + : /^(\d{1,2}):(\d{2})$/; + const match = cleanTime.match(timeRegex); + if (!match) return null; + const hours = parseInt(match[1], 10); + const minutes = parseInt(match[2], 10); + const seconds = includesSeconds ? parseInt(match[3], 10) : undefined; + if (is12Hour ? hours < 1 || hours > 12 : hours < 0 || hours > 23) return null; + if (minutes < 0 || minutes > 59) return null; + if (seconds !== undefined && (seconds < 0 || seconds > 59)) return null; + let adjustedHours = hours; + const period = is12Hour ? match[includesSeconds ? 4 : 3] : undefined; + if (is12Hour) { + if (period?.toUpperCase() === 'PM' && hours !== 12) adjustedHours += 12; + if (period?.toUpperCase() === 'AM' && hours === 12) adjustedHours = 0; + } + return { hours: adjustedHours, minutes, seconds }; +};
7-65
: Remove duplicate parser from this hookKeep parsing logic in one place to avoid divergence from the TimePicker stack.
Apply:
-const parseTimeString = ( - timeStr: string, - format: TimeFormat, -): { hours: number; minutes: number; seconds?: number } | null => { - // ...local implementation... -};
67-69
: Good: comparisons now operate in seconds (format-aware)This addresses earlier boundary issues with 12/24h and seconds.
94-116
: Confirm inclusive bounds semanticsCurrent logic is inclusive (equals passes). Verify this matches product expectations; adjust to <=/>= if exclusivity is desired.
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.
Actionable comments posted: 1
🧹 Nitpick comments (6)
src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts (1)
67-75
: Make wrapNumericValue exhaustive (avoid implicit undefined)Add a default return to protect against future type drift; today it’s safe by union, but runtime will return
undefined
if misused.const wrapNumericValue = (value: number, segmentType: NumericSegmentType, is12Hour: boolean): number => { switch (segmentType) { case 'hours': return wrapHours(value, is12Hour); case 'minutes': case 'seconds': return wrapMinutesSeconds(value); - } + default: + return value; + } };src/app-components/TimePicker/TimePicker.tsx (1)
432-447
: Localize trigger aria-labelReplace hardcoded 'Open time picker' with a localized string.
- aria-label='Open time picker' + aria-label={labels.openPickerAriaLabel ?? 'Open time picker'}src/app-components/TimePicker/utils/timeFormatUtils.ts (4)
28-30
: Normalize AM/PM output for segment renderingTrim and uppercase to ensure consistent display and avoid trailing/leading whitespace issues.
- return value.toString(); + return value.toString().trim().toUpperCase();
50-74
: Trim inputs and use Number.isNaN in parsingImproves robustness for entries like ' am ' and ' 07 '.
export const parseSegmentInput = ( input: string, segmentType: SegmentType, _format: TimeFormat, ): number | string | null => { - if (!input.trim()) { + if (!input.trim()) { return null; } + const normalized = input.trim(); + if (segmentType === 'period') { - const upperInput = input.toUpperCase(); + const upperInput = normalized.toUpperCase(); if (upperInput === 'AM' || upperInput === 'PM') { return upperInput as 'AM' | 'PM'; } return null; } // Parse numeric input - const numValue = parseInt(input, 10); - if (isNaN(numValue)) { + const numValue = Number.parseInt(normalized, 10); + if (Number.isNaN(numValue)) { return null; } return numValue; };
76-116
: Validate against trimmed input; prefer Number.isNaNPrevents false negatives on inputs with spaces and aligns NaN checks.
export const isValidSegmentInput = (input: string, segmentType: SegmentType, format: TimeFormat): boolean => { - if (!input.trim()) { + const normalized = input.trim(); + if (!normalized) { return false; } if (segmentType === 'period') { - const upperInput = input.toUpperCase(); + const upperInput = normalized.toUpperCase(); return upperInput === 'AM' || upperInput === 'PM'; } // Check if it contains only digits - if (!/^\d+$/.test(input)) { + if (!/^\d+$/.test(normalized)) { return false; } - const numValue = parseInt(input, 10); - if (isNaN(numValue)) { + const numValue = Number.parseInt(normalized, 10); + if (Number.isNaN(numValue)) { return false; } // Single digits are always valid (will be auto-padded) - if (input.length === 1) { + if (normalized.length === 1) { return true; }
17-25
: Optional: defend against out‑of‑range hoursIf upstream validation can’t guarantee 0–23, consider normalizing or asserting here; otherwise ignore.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/app-components/TimePicker/TimePicker.tsx
(1 hunks)src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
(1 hunks)src/app-components/TimePicker/types.ts
(1 hunks)src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
(1 hunks)src/app-components/TimePicker/utils/segmentTyping.ts
(1 hunks)src/app-components/TimePicker/utils/timeFormatUtils.ts
(1 hunks)src/layout/TimePicker/useTimePickerValidation.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app-components/TimePicker/types.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts
src/layout/TimePicker/useTimePickerValidation.ts
src/app-components/TimePicker/TimePicker.tsx
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts
src/app-components/TimePicker/utils/segmentTyping.ts
src/app-components/TimePicker/utils/timeFormatUtils.ts
🧬 Code graph analysis (6)
src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts (1)
src/app-components/TimePicker/types.ts (5)
TimeValue
(9-14)SegmentChangeResult
(104-106)NumericSegmentType
(6-6)SegmentConstraints
(22-26)SegmentType
(5-5)
src/layout/TimePicker/useTimePickerValidation.ts (6)
src/app-components/TimePicker/types.ts (2)
TimeFormat
(2-2)TimeValue
(9-14)src/features/validation/index.ts (1)
ComponentValidation
(151-153)src/utils/layout/hooks.ts (1)
useDataModelBindingsFor
(102-112)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/features/formData/FormDataWrite.tsx (1)
FD
(685-1098)src/app-components/TimePicker/utils/timeConstraintUtils.ts (1)
parseTimeString
(3-43)
src/app-components/TimePicker/TimePicker.tsx (9)
src/app-components/TimePicker/types.ts (6)
TimePickerProps
(29-44)DropdownFocusState
(65-69)SegmentType
(5-5)TimeConstraints
(17-20)TimeValue
(9-14)NavigationAction
(71-77)src/app-components/TimePicker/utils/timeConstraintUtils.ts (2)
parseTimeString
(3-43)getSegmentConstraints
(69-176)src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
formatTimeValue
(3-15)src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts (1)
handleSegmentValueChange
(117-136)src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.ts (1)
calculateNextFocusState
(11-68)src/app-components/TimePicker/utils/formatDisplayHour/formatDisplayHour.ts (1)
formatDisplayHour
(7-22)src/app-components/TimePicker/utils/generateTimeOptions/generateTimeOptions.ts (3)
generateHourOptions
(8-20)generateMinuteOptions
(44-44)generateSecondOptions
(27-37)src/app-components/TimePicker/TimeSegment/TimeSegment.tsx (1)
TimeSegment
(10-135)src/app-components/TimePicker/utils/normalizeHour.ts (1)
normalizeHour
(10-20)
src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (3)
src/app-components/TimePicker/types.ts (1)
SegmentInputConfig
(109-116)src/app-components/TimePicker/utils/keyboardNavigation.ts (3)
handleValueIncrement
(68-104)handleValueDecrement
(106-142)handleSegmentKeyDown
(8-52)src/app-components/TimePicker/utils/segmentTyping.ts (4)
clearSegment
(146-149)commitSegmentValue
(154-164)handleSegmentCharacterInput
(168-223)processSegmentBuffer
(108-141)
src/app-components/TimePicker/utils/segmentTyping.ts (1)
src/app-components/TimePicker/types.ts (4)
SegmentTypingResult
(85-88)SegmentType
(5-5)SegmentBuffer
(90-94)TimeFormat
(2-2)
src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
src/app-components/TimePicker/types.ts (3)
TimeValue
(9-14)TimeFormat
(2-2)SegmentType
(5-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
- GitHub Check: Analyze (javascript)
🔇 Additional comments (11)
src/app-components/TimePicker/TimePicker.tsx (4)
667-695
: Disable invalid AM/PM options per min/max constraintsAM/PM buttons should reflect constraints; currently always enabled, allowing invalid period switches.
- {['AM', 'PM'].map((period, optionIndex) => { + {(['AM', 'PM'] as const).map((period, optionIndex) => { + const hour12 = displayHours; // 1-12 + const candidate24 = + hour12 === 12 ? (period === 'AM' ? 0 : 12) : period === 'PM' ? hour12 + 12 : hour12; + const candidate: TimeValue = { ...timeValue, hours: candidate24, period }; + const hourOk = getSegmentConstraints('hours', candidate, constraints, format).validValues.includes( + is12Hour ? hour12 : candidate24, + ); + const minOk = getSegmentConstraints('minutes', candidate, constraints, format).validValues.includes( + timeValue.minutes, + ); + const secOk = !includesSeconds + ? true + : getSegmentConstraints('seconds', candidate, constraints, format).validValues.includes( + timeValue.seconds, + ); + const isDisabled = !(hourOk && minOk && secOk); const isSelected = timeValue.period === period; const columnIndex = includesSeconds ? 3 : 2; const isFocused = dropdownFocus.isActive && dropdownFocus.column === columnIndex && dropdownFocus.option === optionIndex; ... - <button + <button key={period} type='button' - className={`${styles.dropdownOption} ${ + className={`${styles.dropdownOption} ${ isSelected ? styles.dropdownOptionSelected : '' - } ${isFocused ? styles.dropdownOptionFocused : ''}`} + } ${isFocused ? styles.dropdownOptionFocused : ''} ${ + isDisabled ? styles.dropdownOptionDisabled : '' + }`} + disabled={isDisabled} onClick={() => { - handleDropdownPeriodChange(period as 'AM' | 'PM'); + if (!isDisabled) { + handleDropdownPeriodChange(period as 'AM' | 'PM'); + } setDropdownFocus({ column: columnIndex, option: optionIndex, isActive: true, }); }}
496-503
: Hours disabling compares 24h-normalized value against 12h constraints (wrong list membership)
validValues
for 12h are 1–12, but you check a 24hnormalizedHour
. This disables valid options.- const normalizedHour = normalizeHour(option.value, is12Hour, timeValue.period || 'AM'); - - const isDisabled = - constraints.minTime || constraints.maxTime - ? !getSegmentConstraints('hours', timeValue, constraints, format).validValues.includes( - normalizedHour, - ) - : false; + const hourValidValues = getSegmentConstraints('hours', timeValue, constraints, format).validValues; + const isDisabled = + constraints.minTime || constraints.maxTime ? !hourValidValues.includes(option.value) : false;
601-601
: Hardcoded Norwegian label 'Sekunder' — use localized labelUse
segmentLabels.seconds
to respect i18n.- <div className={styles.dropdownLabel}>Sekunder</div> + <div className={styles.dropdownLabel}>{segmentLabels.seconds}</div>
452-452
: Remove aria-modal on non-modal Popover
aria-modal='true'
is for modal dialogs; Popover here is not modal. Drop it (or switch to a Dialog if modal behavior is required).- aria-modal='true'
src/app-components/TimePicker/utils/segmentTyping.ts (2)
154-164
: LGTM — commit defaults are sensible (AM for period, 0 for numeric).
88-97
: LGTM — period typing preserves state on invalid keys.src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts (2)
73-78
: Always commit processed buffer to enable 00 fallback on blurSkipping commit when
actualValue === null
blocks the intended 00 fallback for minutes/seconds.const processed = processSegmentBuffer(bufferValue, segmentType, timeFormat.includes('a')); - if (processed.actualValue !== null) { - const committedValue = commitSegmentValue(segmentType, processed.actualValue); - onValueChange(committedValue); - } + const committedValue = commitSegmentValue(segmentType, processed.actualValue); + onValueChange(committedValue);
34-42
: Avoid double-commit when clearing period (value flips from null to 'AM')You call onValueChange(null) and then immediately commit to 'AM'. Early-return for period to keep the intended cleared state.
function clearCurrentValueAndDisplay() { const clearedSegment = clearSegment(); onUpdateDisplay(clearedSegment.displayValue); - if (segmentType === 'period') { - onValueChange(null); - } - const committedValue = commitSegmentValue(segmentType, clearedSegment.actualValue); - onValueChange(committedValue); + if (segmentType === 'period') { + onValueChange(null); + return; + } + const committedValue = commitSegmentValue(segmentType, clearedSegment.actualValue); + onValueChange(committedValue); }src/layout/TimePicker/useTimePickerValidation.ts (1)
69-77
: LGTM — validates with active format and seconds. Translation keys present in src/language/texts/en.ts, src/language/texts/nb.ts, src/language/texts/nn.ts.src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts (1)
98-112
: Guard: contradictory/empty constraints should clamp instead of “nearest”If
validValues
is empty (e.g., min/max conflict), you currently returnwrappedValue
via nearest, which may violate constraints. Clamp within the [min,max] bounds first.const wrappedValue = wrapNumericValue(value, segmentType, is12Hour); - // Return wrapped value if it's within constraints + // If constraints produce no valid values, clamp within [min,max] + if (segmentConstraints.validValues.length === 0) { + const minBound = Math.min(segmentConstraints.min, segmentConstraints.max); + const maxBound = Math.max(segmentConstraints.min, segmentConstraints.max); + const clamped = Math.max(minBound, Math.min(maxBound, wrappedValue)); + return { updatedTimeValue: { [segmentType]: clamped } }; + } + + // Return wrapped value if it's within constraints if (segmentConstraints.validValues.includes(wrappedValue)) { return { updatedTimeValue: { [segmentType]: wrappedValue }, }; } // Find and return nearest valid value const nearestValid = findNearestValidValue(wrappedValue, segmentConstraints.validValues); return { updatedTimeValue: { [segmentType]: nearestValid }, };src/app-components/TimePicker/utils/timeFormatUtils.ts (1)
3-15
: formatTimeValue: LGTM; AM/PM derived from hours and 12h zero‑padding fixedConfirmed: period is computed from hours and hours are consistently zero‑padded for 12h formats.
|
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.
Ser veldig bra ut! Fant ikke mye å plukke på her, og bra testdekning!!
Description
Dropdown:
Dropdown with seconds:
Dropdown with period:
With min/max constraints:
You can also type the time in the input boxes, or use keyboard up and down in the input boxes to increment or decrement.
Related Issue(s)
Verification/QA
kind/*
andbackport*
label to this PR for proper release notes groupingSummary by CodeRabbit
New Features
Style
Documentation
Tests