angular / angular/components

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

Open
#31,803 2 comments 0 reactions 0 assignees View on GitHub
area: material/timepicker P3
Dominant language
TypeScript
Stars
25k
Forks
6.8k
Avg merge
1d 8h
Merged PRs (30d)
91

Description

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

Contributor guide

Open the contributing guide

Research direction

Start at the timepicker's _assignUserSelection logic and reproduce the 2025-10-26 Europe/Vienna case using the linked StackBlitz. Trace how the last valid date and extracted time are combined, then verify that selecting DST and standard-time 02:00 remains distinct and that the expected behavior holds with the custom Luxon adapter.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.