Skip to content

Conversation

adamhaeger
Copy link
Contributor

@adamhaeger adamhaeger commented Aug 14, 2025

Description

  • Added a full TimePicker: 24h/12h formats, optional seconds, AM/PM, dropdown selector, keyboard navigation, responsive styling, summary rendering, min/max constraints, and validation.

Dropdown:

Screenshot 2025-09-22 at 14 36 38

Dropdown with seconds:

Screenshot 2025-09-22 at 14 36 53

Dropdown with period:

Screenshot 2025-09-22 at 14 37 05

With min/max constraints:

Screenshot 2025-09-22 at 14 37 28

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

  • Manual functionality testing
    • I have tested these changes manually
    • Creator of the original issue (or service owner) has been contacted for manual testing (or will be contacted when released in alpha)
    • No testing done/necessary
  • Automated tests
    • Unit test(s) have been added/updated
    • Cypress E2E test(s) have been added/updated
    • No automatic tests are needed here (no functional changes/additions)
    • I want someone to help me make some tests
  • UU/WCAG (follow these guidelines until we have our own)
    • I have tested with a screen reader/keyboard navigation/automated wcag validator
    • No testing done/necessary (no DOM/visual changes)
    • I want someone to help me perform accessibility testing
  • User documentation @ altinn-studio-docs
    • Has been added/updated
    • No functionality has been changed/added, so no documentation is needed
    • I will do that later/have created an issue
  • Support in Altinn Studio
    • Issue(s) created for support in Studio
    • This change/feature does not require any changes to Altinn Studio
  • Sprint board
    • The original issue (or this PR itself) has been added to the Team Apps project and to the current sprint board
    • I don't have permissions to do that, please help me out
  • Labels
    • I have added a kind/* and backport* label to this PR for proper release notes grouping
    • I don't have permissions to add labels, please help me out

Summary by CodeRabbit

  • New Features

    • Introduced a TimePicker component with segmented inputs, dropdown selection, 12h/24h formats (with optional seconds), min/max constraints, and validation with localized messages.
    • Added summary rendering for TimePicker in form overviews.
    • Expanded localization (EN, NB, NN) for time labels and validation messages.
  • Style

    • New responsive and accessible styling for TimePicker, including dropdown UI and focus states.
  • Documentation

    • Added TimePicker README with usage, formats, accessibility, and behavior details.
  • Tests

    • Comprehensive tests for TimePicker UI, responsiveness, typing, keyboard navigation, formatting, constraints, and utilities.

@adamhaeger
Copy link
Contributor Author

/publish

Copy link
Contributor

github-actions bot commented Aug 14, 2025

PR release:

  • <link rel="stylesheet" type="text/css" href="https://altinncdn.no/toolkits/altinn-app-frontend/4.21.0-pr.1927.1261-timepicker.19a7af92/altinn-app-frontend.css">
  • <script src="https://altinncdn.no/toolkits/altinn-app-frontend/4.21.0-pr.1927.1261-timepicker.19a7af92/altinn-app-frontend.js"></script>

⚙️ Building...
✅ Done!

@adamhaeger adamhaeger added kind/product-feature Pull requests containing new features backport-ignore This PR is a new feature and should not be cherry-picked onto release branches labels Aug 14, 2025
Copy link

coderabbitai bot commented Aug 22, 2025

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Localization updates
src/language/texts/en.ts, src/language/texts/nb.ts, src/language/texts/nn.ts
Added seven time-related translation keys per locale (validation messages with {0} placeholders and segment labels).
Layout integration for TimePicker
src/layout/TimePicker/index.tsx, src/layout/TimePicker/TimePickerComponent.tsx, src/layout/TimePicker/TimePickerSummary.tsx, src/layout/TimePicker/config.ts, src/layout/TimePicker/useTimePickerValidation.ts, src/layout/TimePicker/TimePickerComponent.test.tsx, src/features/expressions/shared-tests/functions/displayValue/type-TimePicker.json
New layout-level TimePicker class and wrapper component, config/schema, summary renderer, validation hook wired into layout, component tests, and displayValue fixture.
App component core
src/app-components/TimePicker/TimePicker.tsx, src/app-components/TimePicker/TimePicker.module.css, src/app-components/TimePicker/README.md
New TimePicker UI implementation with popover dropdown, responsive CSS, and component documentation.
TimeSegment component and hooks
src/app-components/TimePicker/TimeSegment/TimeSegment.tsx, src/app-components/TimePicker/TimeSegment/TimeSegment.test.tsx, src/app-components/TimePicker/TimeSegment/hooks/useSegmentDisplay.ts, src/app-components/TimePicker/TimeSegment/hooks/useSegmentInputHandlers.ts, src/app-components/TimePicker/TimeSegment/hooks/useTypingBuffer.ts, src/app-components/TimePicker/TimeSegment/hooks/useTimeout.ts
New TimeSegment input component, behavior hooks (display sync, input handling, typing buffer, timeout), and segment behavior tests.
Utilities: types, formatting, parsing, constraints, navigation
src/app-components/TimePicker/types.ts, src/app-components/TimePicker/utils/keyboardNavigation.ts, src/app-components/TimePicker/utils/segmentTyping.ts, src/app-components/TimePicker/utils/timeConstraintUtils.ts, src/app-components/TimePicker/utils/timeFormatUtils.ts, src/app-components/TimePicker/utils/normalizeHour.ts, src/app-components/TimePicker/utils/generateTimeOptions/generateTimeOptions.ts, src/app-components/TimePicker/utils/formatDisplayHour/formatDisplayHour.ts, src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.ts, src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.ts
Added comprehensive TimePicker types and multiple utility modules for keyboard navigation, segment typing/coercion, constraints/range logic, formatting/parsing, option generation, hour normalization, segment value changes, and focus-state calculation.
Utility tests
src/app-components/TimePicker/utils/keyboardNavigation.test.ts, src/app-components/TimePicker/utils/generateTimeOptions/generateTimeOptions.test.ts, src/app-components/TimePicker/utils/formatDisplayHour/formatDisplayHour.test.ts, src/app-components/TimePicker/utils/timeConstraintUtils.test.ts, src/app-components/TimePicker/utils/timeFormatUtils.test.ts, src/app-components/TimePicker/utils/handleSegmentValueChange/handleSegmentValueChange.test.ts, src/app-components/TimePicker/utils/calculateNextFocusState/calculateNextFocusState.test.ts
Added unit tests covering keyboard navigation, option generation, hour formatting, constraints, formatting/validation utilities, segment value changes, and focus navigation.
Component-level tests & responsive checks
src/app-components/TimePicker/TimePicker.responsive.test.tsx, src/layout/TimePicker/TimePickerComponent.test.tsx
Added responsive and component integration tests (accessibility, breakpoints, format variants, readOnly/disabled).
Date component logging
src/layout/Date/DateComponent.tsx
Two console.log debug statements added (logs input value and computed displayData).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The changeset contains at least one out-of-scope edit: two debugging console.log statements were added to src/layout/Date/DateComponent.tsx, which are unrelated to the TimePicker feature and should not be present in this feature PR. Remove the console.log statements from src/layout/Date/DateComponent.tsx, run linters/tests, and amend the PR (or move that unrelated debug change to a separate follow-up) so the feature branch only contains TimePicker-related changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Feat/1261 timepicker" is concise and directly reflects the main change (adding a TimePicker feature) and references the linked issue, so it clearly summarizes the primary purpose of the changeset for a reviewer scanning history.
Linked Issues Check ✅ Passed The implementation addresses the primary coding objectives from the linked issue [#1261]: it provides a TimePicker for selecting hours and minutes, includes data binding to a single controlled value, validation, min/max constraints, keyboard navigation, summary rendering and unit tests; the PR does not explicitly add separate hour/minute datamodel bindings but the linked issue permitted either approach so the delivered single-string binding is compliant.
Description Check ✅ Passed The PR description follows the repository template: it contains a clear summary, includes "closes #1261", provides screenshots, and a populated Verification/QA checklist with manual testing, unit tests, and accessibility checks; a few checklist items remain unchecked (notably Cypress E2E and full user documentation), which are noted in the description.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/1261-timepicker

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e691bf and 9722ecb.

📒 Files selected for processing (1)
  • src/app-components/TimePicker/utils/timeFormatUtils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app-components/TimePicker/utils/timeFormatUtils.ts
⏰ 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

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@adamhaeger adamhaeger marked this pull request as ready for review August 22, 2025 13:47
Copy link

@coderabbitai coderabbitai bot left a 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 message

To 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 messages

The 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 test

This 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 too

Asserting 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 consistency

Attach 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 utils

This 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 reuse

extractTimeFromValue 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 warning

Local 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 assertions

Expand 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 DOM

After 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 via unknown.
 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 with as 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: Import TimeValue 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 of clear() 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 with rerender to avoid multiple textboxes.

Using screen.getAllByRole('textbox')[1] is a bit brittle as other tests evolve. A rerender 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 in minTime/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 stores now with local time components as an ISO string (UTC). When round-tripping, you convert back using local Date, 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] without Z) 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 in parseSegmentInput 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 container

Centering 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 strings

If 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 background

Hardcoded 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 missing

Relying 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 width

Min-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 utility

Formatting 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 destructuring const [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 onKeyDown

Relying on onKeyPress will become brittle across browsers/React versions. Migrate to onBeforeInput for character input or handle characters in onKeyDown.

High-level approach:

  • Replace onKeyPress with onBeforeInput={(e) => { const char = e.data; ... }}.
  • Keep non-character logic in onKeyDown.

20-36: Props min/max are unused

They 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 maxLength

Both branches are 2.

Apply this diff:

-        maxLength={type === 'period' ? 2 : 2}
+        maxLength={2}
src/app-components/TimePicker/segmentTyping.ts (7)

208-214: Robustness: guard parseInt result before comparison in shouldAdvanceSegment

If buffer is ever non-numeric in this path, digit becomes NaN 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-insensitive

Using format.includes('a') misses A 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 for 00 → current behavior sets 01, not 12

In 12h mode with first digit 0 and second 0, you coerce to '01'. Many time pickers treat 00 as 12 (12 AM/PM). If your UX spec expects 12, 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 returns shouldAdvance: false. That’s consistent with your comment (“Chrome behavior”) and shouldAdvanceSegment() returning false for minutes/seconds. If design ever changes to auto-advance after two digits, you can flip the return to true when currentBuffer.length === 1.


120-146: Minor: underscore _is12Hour is unused

The underscore suggests intentional, but if not planned for future use, consider removing the parameter to avoid confusion.


1-279: Consolidate segment constraints to avoid duplication

Ranges are currently codified in multiple places (processHourInput, processMinuteInput, coerceToValidRange, and shouldAdvanceSegment). 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 format

To make commitSegmentValue default hours correctly in 12-hour mode, add an optional is12Hour flag (defaulting to false) 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 of commitSegmentValue(buffer.actualValue, type) and change to pass the 12h flag based on your format 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) returns 12.

📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between a66d212 and 6a1d912.

📒 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 using any 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 via queryOptions

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 from src/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 in config.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

Copy link

@coderabbitai coderabbitai bot left a 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 removed

The 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 theming

Replace 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 accessibility

Negative 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 instead

Signals 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-motion

You’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) and timepicker.* (labels). If intentional to mirror existing date_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 relationships

If the dropdown is aria-controlled by the trigger, add a note recommending aria-controls and aria-expanded on the trigger, and role="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 timers

Some 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 indexing

Selecting the second textbox by index can break if test order or render tree changes. Prefer scoping via within(container) or adding aria-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 drift

Duplicate 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 utils

These 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 parsing

parseSegmentInput 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 brittle

Using 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 needed

If 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 normalization

Without trimming, values like ' am ' won’t parse.

-    const upperInput = input.toUpperCase();
+    const upperInput = input.trim().toUpperCase();

28-33: Optional: normalize period casing on output

For 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–23

getInitialHighlightIndex 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 options

If 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 readability

Minor: compute timeInSeconds directly without the intermediate minutes var.


21-56: Optional: centralize 12h⇄24h conversion

parseTimeString embeds the conversion; extracting helpers would reduce duplication across modules and test fixtures.

src/app-components/TimePicker/components/TimeSegment.tsx (3)

272-279: Prefer onBeforeInput/onKeyDown over onKeyPress (deprecated).

onKeyPress is deprecated in modern React/DOM. Consider moving character handling to onBeforeInput (for text insertion) and keeping navigation/increment on onKeyDown. 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 for maxLength.

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 to handleValueIncrement/Decrement, arrows will skip disabled values immediately. That requires adding a constraints?: SegmentConstraints prop or validValues?: number[] to TimeSegmentProps and wiring it from TimePicker. 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() inside handleSegmentKeyDown. Returning a preventDefault 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 to nextIndex (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; use labels 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 no aria-labelledby or aria-label. Provide an accessible name. If you don’t have a visible heading, add aria-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 alongside aria-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 when hours.validValues is empty (and minutes/seconds compatible). Want me to draft it?

src/app-components/TimePicker/utils/segmentTyping.ts (1)

114-116: Tab in isNavigationKey is ineffective here and could be misleading.

isNavigationKey includes 'Tab', but you only call it from onKeyPress (or planned onBeforeInput). Tab is processed on keydown and will not be handled here.

Either remove 'Tab' from isNavigationKey or handle Tab 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 6a1d912 and 8ec76f2.

📒 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 using any 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 via queryOptions

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 from src/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 needed

These 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 any userEvent. 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 utilities

Good 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 cases

Overall, 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 tests

Clear 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 good

Round/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 segments

Good 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 tests

I confirmed that both coerceToValidRange and shouldAdvanceSegment are explicitly imported and exercised in src/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 into TimeSegment, feel free to invoke these helpers there, but they must remain defined for the current test suite.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a 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, or shouldScrollToOption, 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 that period 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 helpers

Our 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec76f2 and ae7e785.

📒 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 using any 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 via queryOptions

Files:

  • src/app-components/TimePicker/tests/dropdownBehavior.test.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

In tests, use renderWithProviders from src/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

Copy link

@coderabbitai coderabbitai bot left a 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 of nextIndex.

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 24h actualHour before includes(...). Compare using the option’s native 12h value. Also, reuse isOptionDisabled 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 shared parseTimeString 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 utils

Also 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.

📥 Commits

Reviewing files that changed from the base of the PR and between ae7e785 and ecceadb.

📒 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 using any 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 via queryOptions

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

Copy link

@coderabbitai coderabbitai bot left a 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 with a (AM/PM). For 12-hour formats, use hh.

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 introduced

Adding an explicit token reference reduces confusion and prevents misuse (e.g., HH vs hh).

 ## 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/format

Make it explicit that onChange emits a string in the active format, and what value 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 list

Small 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 Input

Minor 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 0x

Also 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 Usage

This helps integrators understand how value and onChange interplay.

 ## Usage
@@
 ```tsx
@@
 </>

+Note: The component is controlled. When value is provided, the displayed time mirrors that value; user edits are surfaced via onChange(value), and it’s the parent’s responsibility to update value. If value is omitted, the component manages its internal state but still calls onChange 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 -->

Copy link

@coderabbitai coderabbitai bot left a 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 respects labels 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 the has(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-op onChange 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 with true.

🧹 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 over timeValue 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 with role='spinbutton') or remove.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6b0372 and bb99f85.

📒 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 using any 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 via queryOptions

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

Copy link

@coderabbitai coderabbitai bot left a 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 correctly

Clear 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 check

Minor 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 identities

Wrap 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 validate

Prevents 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 completeness

You pass _is12Hour but don’t use it; could fine-tune isComplete for hours. Low priority.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb99f85 and 9ce3607.

📒 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 using any 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 via queryOptions

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 correct

AM 12→0, PM 1–11→13–23, and passthrough for 24h are correct.

Copy link

@coderabbitai coderabbitai bot left a 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 stability

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9ce3607 and d504073.

📒 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 using any 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 via queryOptions

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 values

The condition if (processed.actualValue !== null) prevents committing null 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);

Copy link

@coderabbitai coderabbitai bot left a 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 code

Drop 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-describedby

If 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 and aria-describedby from the Label helpers into TimePickerControl?

src/app-components/TimePicker/utils/timeConstraintUtils.test.ts (1)

9-13: Use the exported SegmentConstraints type instead of redefining it

Importing 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 checks value === null but the function’s type is number | string, and call sites guard against null. Either accept null 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 remove

TimeSegmentProps 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

📥 Commits

Reviewing files that changed from the base of the PR and between d504073 and 8d79046.

📒 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 using any 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 via queryOptions

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 in config.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 from src/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 using getUTCHours()?

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 -l

If missing, run the repo’s codegen and commit the generated file.


5-57: Is timeStamp 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/max

Period 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 parsing

Range 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 value

This 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 00

The 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 with period. 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 no onChange. Ensure readOnly is true at the DOM level; keyboard handling still works via onKeyDown.

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 avoid any.

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.

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8d79046 and ec5d790.

📒 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 using any 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 via queryOptions

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 copy

Prefer 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 hook

Keep 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 semantics

Current logic is inclusive (equals passes). Verify this matches product expectations; adjust to <=/>= if exclusivity is desired.

Copy link

@coderabbitai coderabbitai bot left a 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-label

Replace 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 rendering

Trim 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 parsing

Improves 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.isNaN

Prevents 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 hours

If 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

📥 Commits

Reviewing files that changed from the base of the PR and between ec5d790 and 4a67ee6.

📒 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 using any 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 via queryOptions

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 constraints

AM/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 24h normalizedHour. 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 label

Use 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 blur

Skipping 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 return wrappedValue 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 fixed

Confirmed: period is computed from hours and hours are consistently zero‑padded for 12h formats.

Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copy link
Contributor

@paal2707 paal2707 left a 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!!

@adamhaeger adamhaeger merged commit e1ff693 into main Sep 24, 2025
15 checks passed
@adamhaeger adamhaeger deleted the feat/1261-timepicker branch September 24, 2025 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport-ignore This PR is a new feature and should not be cherry-picked onto release branches kind/product-feature Pull requests containing new features
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Time picker
2 participants