CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-stately--datepicker

React state management hooks for date picker components with internationalization and accessibility support.

Pending
Overview
Eval results
Files

time-field-state.mddocs/

Time Field State

State management for time field components that allow users to enter and edit time values. Each part of a time value (hour, minute, second, AM/PM) is displayed in individually editable segments, providing precise keyboard-based time input.

Capabilities

useTimeFieldState Hook

Creates a state object for managing time field component state. Extends date field functionality specifically for time-only input scenarios.

/**
 * Provides state management for a time field component.
 * A time field allows users to enter and edit time values using a keyboard.
 * Each part of a time value is displayed in an individually editable segment.
 * @param props - Configuration options including locale
 * @returns TimeFieldState object extending DateFieldState with time-specific functionality
 */
function useTimeFieldState<T extends TimeValue = TimeValue>(
  props: TimeFieldStateOptions<T>
): TimeFieldState;

interface TimeFieldStateOptions<T extends TimeValue = TimeValue> extends TimePickerProps<T> {
  /** The locale to display and edit the value according to. */
  locale: string;
}

interface TimeFieldState extends DateFieldState {
  /** The current time value as a Time object. */
  timeValue: Time;
}

Usage Examples:

import { useTimeFieldState } from "@react-stately/datepicker";
import { Time } from "@internationalized/date";

// Basic time field
function BasicTimeField() {
  const state = useTimeFieldState({
    locale: 'en-US',
    defaultValue: new Time(14, 30), // 2:30 PM
    onChange: (time) => console.log("Selected time:", time?.toString())
  });

  return (
    <div>
      {state.segments.map((segment, index) => (
        <span
          key={index}
          style={{
            padding: '2px 4px',
            margin: '0 1px',
            backgroundColor: segment.isPlaceholder ? '#f0f0f0' : 'white',
            border: segment.isEditable ? '1px solid #ccc' : 'none',
            borderRadius: '2px'
          }}
        >
          {segment.text}
        </span>
      ))}
      <div>
        Current time: {state.timeValue.toString()} 
        ({state.timeValue.hour}:{state.timeValue.minute.toString().padStart(2, '0')})
      </div>
    </div>
  );
}

// 12-hour time field with AM/PM
function TwelveHourTimeField() {
  const state = useTimeFieldState({
    locale: 'en-US',
    granularity: 'minute',
    hourCycle: 12,
    onChange: (time) => {
      if (time) {
        const hours = time.hour;
        const minutes = time.minute;
        const period = hours >= 12 ? 'PM' : 'AM';
        const displayHour = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
        console.log(`${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`);
      }
    }
  });

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center' }}>
        {state.segments.map((segment, index) => (
          <input
            key={index}
            value={segment.text}
            placeholder={segment.placeholder}
            readOnly={!segment.isEditable}
            onChange={(e) => {
              if (segment.type === 'dayPeriod') {
                // Handle AM/PM toggle
                const currentHour = state.timeValue.hour;
                const newHour = e.target.value.toUpperCase() === 'PM' && currentHour < 12
                  ? currentHour + 12
                  : e.target.value.toUpperCase() === 'AM' && currentHour >= 12
                  ? currentHour - 12
                  : currentHour;
                state.setSegment('hour', newHour);
              } else {
                const num = parseInt(e.target.value);
                if (!isNaN(num)) {
                  state.setSegment(segment.type, num);
                }
              }
            }}
            style={{
              width: `${Math.max(segment.text.length, 2)}ch`,
              border: segment.isEditable ? '1px solid #ccc' : 'none',
              textAlign: 'center',
              backgroundColor: segment.isPlaceholder ? '#f8f8f8' : 'white'
            }}
          />
        ))}
      </div>
      
      <div style={{ marginTop: '8px' }}>
        <button onClick={() => state.increment('hour')}>Hour +</button>
        <button onClick={() => state.decrement('hour')}>Hour -</button>
        <button onClick={() => state.increment('minute')}>Min +</button>
        <button onClick={() => state.decrement('minute')}>Min -</button>
      </div>
    </div>
  );
}

// Time field with seconds and validation
function PreciseTimeField() {
  const state = useTimeFieldState({
    locale: 'en-US',
    granularity: 'second',
    minValue: new Time(9, 0, 0), // 9:00:00 AM
    maxValue: new Time(17, 0, 0), // 5:00:00 PM
    isRequired: true,
    validate: (time) => {
      if (!time) return "Time is required";
      
      // Custom validation: no times ending in 13 seconds
      if (time.second === 13) {
        return "Unlucky second not allowed";
      }
      
      return null;
    },
    onChange: (time) => {
      if (time) {
        console.log(`${time.hour}:${time.minute}:${time.second}`);
      }
    }
  });

  return (
    <div>
      <div style={{ 
        padding: '8px', 
        border: state.isInvalid ? '2px solid red' : '1px solid #ccc',
        borderRadius: '4px'
      }}>
        {state.segments.map((segment, index) => (
          <span
            key={index}
            tabIndex={segment.isEditable ? 0 : -1}
            onKeyDown={(e) => {
              if (segment.isEditable) {
                switch (e.key) {
                  case 'ArrowUp':
                    e.preventDefault();
                    state.increment(segment.type);
                    break;
                  case 'ArrowDown':
                    e.preventDefault();
                    state.decrement(segment.type);
                    break;
                  case 'PageUp':
                    e.preventDefault();
                    state.incrementPage(segment.type);
                    break;
                  case 'PageDown':
                    e.preventDefault();
                    state.decrementPage(segment.type);
                    break;
                }
              }
            }}
            style={{
              padding: '2px 4px',
              margin: '0 1px',
              backgroundColor: segment.isPlaceholder ? '#f0f0f0' : 'white',
              border: segment.isEditable ? '1px solid #ddd' : 'none',
              borderRadius: '2px',
              outline: 'none',
              cursor: segment.isEditable ? 'text' : 'default'
            }}
          >
            {segment.text}
          </span>
        ))}
      </div>
      
      {state.isInvalid && (
        <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>
          Please enter a valid time between 9:00:00 AM and 5:00:00 PM
        </div>
      )}
      
      <div style={{ marginTop: '8px' }}>
        <button onClick={() => state.setValue(new Time(12, 0, 0))}>
          Set to Noon
        </button>
        <button onClick={() => state.confirmPlaceholder()}>
          Confirm Current
        </button>
        <button onClick={() => state.setValue(null)}>
          Clear
        </button>
      </div>
    </div>
  );
}

Time Value Management

The time field state manages time values specifically, converting between different time representations.

interface TimeFieldState extends DateFieldState {
  /** The current time value as a Time object from @internationalized/date */
  timeValue: Time;
}

The timeValue property provides direct access to the time portion regardless of whether the underlying value includes date information.

Time-specific Features

Unlike date fields, time fields have specific behavior for time-only scenarios:

  • Hour Cycle Support: Automatic 12/24 hour format handling
  • AM/PM Management: Intelligent day period segment handling
  • Time Zone Handling: Supports time-only values and datetime values with time zones
  • Placeholder Time: Uses sensible defaults (midnight) when no time is specified

Integration with Date Values

Time fields can work with different value types:

interface TimeFieldStateOptions<T extends TimeValue> {
  /** Current time value - can be Time, CalendarDateTime, or ZonedDateTime */
  value?: T | null;
  /** Default time value */
  defaultValue?: T | null;
  /** Placeholder value that affects formatting */
  placeholderValue?: Time;
}

Value Type Handling:

// Time-only value
const timeOnlyState = useTimeFieldState({
  locale: 'en-US',
  value: new Time(14, 30, 0), // 2:30:00 PM
});

// DateTime value (time portion will be extracted)
const dateTimeState = useTimeFieldState({
  locale: 'en-US', 
  value: new CalendarDateTime(2023, 6, 15, 14, 30, 0),
});

// Zoned DateTime value (time portion with timezone)
const zonedState = useTimeFieldState({
  locale: 'en-US',
  value: toZoned(new CalendarDateTime(2023, 6, 15, 14, 30), 'America/New_York'),
});

Granularity Control

Control which time segments are displayed and editable:

interface TimeFieldStateOptions<T> {
  /** The smallest time unit to display @default 'minute' */
  granularity?: 'hour' | 'minute' | 'second';
  /** Whether to display in 12 or 24 hour format */
  hourCycle?: 12 | 24;
}

Granularity Examples:

  • granularity: 'hour' - Shows only hour and AM/PM
  • granularity: 'minute' - Shows hour, minute, and AM/PM
  • granularity: 'second' - Shows hour, minute, second, and AM/PM

Time Validation

Time fields support validation with time-specific constraints:

interface TimeFieldStateOptions<T> {
  /** Minimum allowed time */
  minValue?: TimeValue;
  /** Maximum allowed time */
  maxValue?: TimeValue;
  /** Custom validation function */
  validate?: (value: MappedTimeValue<T> | null) => ValidationError | true | null | undefined;
  /** Whether the time field is required */
  isRequired?: boolean;
}

Common Validation Scenarios:

// Business hours validation
const businessHoursState = useTimeFieldState({
  locale: 'en-US',
  minValue: new Time(9, 0),    // 9:00 AM
  maxValue: new Time(17, 0),   // 5:00 PM
  validate: (time) => {
    if (time && (time.minute % 15 !== 0)) {
      return "Please select times in 15-minute intervals";
    }
    return null;
  }
});

// No midnight allowed
const noMidnightState = useTimeFieldState({
  locale: 'en-US',
  validate: (time) => {
    if (time && time.hour === 0 && time.minute === 0) {
      return "Midnight is not allowed";
    }
    return null;
  }
});

Inherited DateFieldState Functionality

TimeFieldState extends DateFieldState, providing all segment manipulation capabilities:

// All DateFieldState methods are available:
state.increment('hour');
state.decrement('minute');
state.incrementPage('hour'); // +2 hours
state.decrementPage('minute'); // -15 minutes
state.setSegment('hour', 14);
state.clearSegment('minute');
state.confirmPlaceholder();

Install with Tessl CLI

npx tessl i tessl/npm-react-stately--datepicker

docs

date-field-state.md

date-picker-state.md

date-range-picker-state.md

index.md

time-field-state.md

tile.json