Skip to content

bug(timepicker): Timepicker DST edge case: incorrect hour mapping with fixed timezone + custom Luxon adapter #31803

@zualexander

Description

@zualexander

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

No response

Description

When using Angular’s timepicker with a shared FormControl, Luxon as the date adapter (with a fixed timezone and locale), there is an edge case when switching from DST → standard time.

The problem arises because _assignUserSelection uses getValidDateOrNull and then applies extracted hours/minutes/seconds onto a target date that may carry the wrong DST context. This causes the two distinct 02:00 times (DST vs standard) on the fallback day to collapse into one — selecting the non-DST 02:00 ends up mapped to the DST 02:00.

Custom DateAdapter:

To ensure consistent Luxon DateTime handling, I replaced the default LuxonDateAdapter with a custom one.

@Injectable()
export class CustomDateAdapter extends LuxonDateAdapter {
  protected override locale = DateTimeService.LOCALE;

  constructor() {
    super(DATE_FORMATS);
  }

  /** Ensure datepicker provides Luxon DateTimes in the default zone */
  public override createDate(year: number, month: number, date: number): DateTime {
    return super.createDate(year, month, date)
      .setZone(DateTimeService.DEFAULT_ZONE)
      .setLocale(DateTimeService.LOCALE);
  }

  /**
   * Needed so the timepicker does not treat DST and non-DST versions of the same wall time as equal.
   * Without this, both values are considered the same and the DST one always “wins”.
   */
  public override sameTime(first: DateTime | null, second: DateTime | null): boolean {
    if (!isDefined(first) || !isDefined(second)) {
      return false;
    }
    return +first === +second;
  }

  public override clone(date: DateTime): DateTime {
    let cloned = super.clone(date);
    cloned = cloned.setZone(DateTimeService.DEFAULT_ZONE);
    cloned = cloned.setLocale(DateTimeService.LOCALE);
    return cloned;
  }
}
´´´

Despite these adjustments, the underlying `_assignUserSelection` logic in the timepicker still collapses the two 02:00 times into one, because of how it uses the last valid date + setTime.

### Reproduction

StackBlitz link: https://stackblitz.com/edit/stackblitz-starters-fyaws5vk
Steps to reproduce:
* Use the custom CustomDateAdapter (above) instead of the default LuxonDateAdapter.
On the DST end day ( 2025-10-26), try selecting both 02:00 (DST) and 02:00 (STD).
* Observe that selecting the 02:00 (STD) still results in the 02:00 (DST) value being applied (or both depending on Custom vs LuxonDateAdapter).

### Expected Behavior

* The timepicker should treat 02:00 DST and 02:00 STD as distinct values when using a fixed timezone.
* Selecting the standard-time 02:00 should not be coerced into the DST-time 02:00.

### Actual Behavior

* Because `_assignUserSelection` rebuilds the target date and applies extracted hours, the ambiguous hour is resolved to the earlier offset (DST).
* Even with a custom adapter that distinguishes times (`sameTime` override), the timepicker ends up normalizing both selections to the DST instance.

### Environment

- Angular: 20.2
- CDK/Material: 20.2
- Browser(s): Chrome, ...
- Operating System (e.g. Windows, macOS, Ubuntu): macOS

- Custom DateAdapter: subclass of LuxonDateAdapter (see above)
- Timezone: Europe/Vienna

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs triageThis issue needs to be triaged by the team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions