• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pglejzer / timepicker-ui / 21551946064

31 Jan 2026 10:26PM UTC coverage: 80.696% (-0.1%) from 80.798%
21551946064

Pull #112

github

web-flow
Merge ed52459e2 into d53ddb7bc
Pull Request #112: fix clock hand lags

2144 of 2965 branches covered (72.31%)

Branch coverage included in aggregate %.

17 of 23 new or added lines in 3 files covered. (73.91%)

16 existing lines in 3 files now uncovered.

2818 of 3184 relevant lines covered (88.51%)

32.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.85
/app/src/managers/clock/controller/ClockController.ts
1
import type { ClockState, ClockType, ClockMode, DisabledTimeConfig, EngineInput, Point } from '../types';
2
import { ClockEngine } from '../engine/ClockEngine';
7✔
3
import { ClockRenderer } from '../renderer/ClockRenderer';
7✔
4

7✔
5
export interface ClockControllerCallbacks {
6
  onHourChange?: (hour: string) => void;
19!
7
  onMinuteChange?: (minute: string) => void;
58✔
8
}
58✔
9

58✔
10
export class ClockController {
58✔
11
  private state: ClockState;
58✔
12
  private renderer: ClockRenderer;
58✔
13
  private clockType: ClockType;
58✔
14
  private disabledTime: DisabledTimeConfig | null;
58✔
15
  private incrementHours: number;
58✔
16
  private incrementMinutes: number;
58✔
17
  private smoothHourSnap: boolean;
58✔
18
  private isDragging: boolean = false;
19
  private callbacks: ClockControllerCallbacks;
20
  private lastProcessedX: number | null = null;
2✔
21
  private lastProcessedY: number | null = null;
2!
UNCOV
22

×
23
  constructor(
24
    renderer: ClockRenderer,
2✔
25
    initialState: ClockState,
2✔
26
    clockType: ClockType,
2✔
27
    disabledTime: DisabledTimeConfig | null,
28
    incrementHours: number = 1,
29
    incrementMinutes: number = 1,
30
    smoothHourSnap: boolean = true,
31
    callbacks: ClockControllerCallbacks = {},
32
  ) {
33
    this.renderer = renderer;
34
    this.state = { ...initialState };
35
    this.clockType = clockType;
36
    this.disabledTime = disabledTime;
37
    this.incrementHours = incrementHours;
38
    this.incrementMinutes = incrementMinutes;
39
    this.smoothHourSnap = smoothHourSnap;
2✔
40
    this.callbacks = callbacks;
2!
UNCOV
41
  }
×
42

43
  handlePointerMove(pointerPos: Point, clockCenter: Point, clockRadius: number): void {
2✔
44
    this.isDragging = true;
1✔
45

1✔
46
    if (this.lastProcessedX === pointerPos.x && this.lastProcessedY === pointerPos.y) {
1✔
47
      return;
1!
NEW
48
    }
×
NEW
49

×
50
    this.lastProcessedX = pointerPos.x;
51
    this.lastProcessedY = pointerPos.y;
1!
52

1✔
53
    const input: EngineInput = {
54
      pointerPosition: pointerPos,
55
      clockCenter,
56
      clockRadius,
1✔
57
      mode: this.state.mode,
1✔
58
      clockType: this.clockType,
1✔
59
      amPm: this.state.amPm,
1✔
60
      disabledTime: this.disabledTime,
1✔
61
      incrementHours: this.incrementHours,
1!
62
      incrementMinutes: this.incrementMinutes,
1✔
63
      smoothHourSnap: this.smoothHourSnap,
64
      currentHour: this.state.hour,
65
    };
2✔
66

2✔
67
    const output = ClockEngine.processPointerInput(input);
68

69
    if (!output.isValid) {
1✔
70
      return;
1✔
71
    }
1✔
72

73
    if (this.state.mode === 'hours') {
74
      const prevHour = this.state.hour;
2✔
75
      this.state.hour = output.value;
1✔
76
      this.state.hourAngle = output.angle;
1✔
77

1✔
78
      if (this.clockType === '24h' && output.isInnerCircle !== undefined) {
1✔
79
        this.renderer.setCircleSize(true);
80
        this.renderer.setCircle24hMode(output.isInnerCircle);
81
      }
45✔
82

45✔
83
      if (this.callbacks.onHourChange && prevHour !== output.value) {
45✔
84
        this.callbacks.onHourChange(output.value);
45✔
85
      }
5✔
86
    } else {
5✔
87
      const prevMinute = this.state.minute;
5✔
88
      this.state.minute = output.value;
5✔
89
      this.state.minuteAngle = output.angle;
90

91
      this.renderer.setCircleSize(true);
40✔
92
      this.renderer.setCircle24hMode(false);
40✔
93

94
      if (this.callbacks.onMinuteChange && prevMinute !== output.value) {
45✔
95
        this.callbacks.onMinuteChange(output.value);
45✔
96
      }
97
    }
98

9✔
99
    this.renderer.setHandAngle(output.angle);
9✔
100
    this.renderer.setActiveValue(output.value);
5✔
101
  }
5✔
102

5!
103
  handlePointerUp(): void {
×
104
    this.isDragging = false;
×
NEW
105
    this.lastProcessedX = null;
×
NEW
106
    this.lastProcessedY = null;
×
107
  }
108

109
  snapToNearestHour(): void {
5✔
110
    if (this.state.mode !== 'hours') return;
111

112
    const targetAngle = ClockEngine.valueToAngle(this.state.hour, 'hours', this.clockType);
113
    this.state.hourAngle = targetAngle;
4✔
114
    this.renderer.animateToAngle(targetAngle);
4✔
115
  }
4✔
116

4✔
117
  switchMode(mode: ClockMode): void {
118
    this.state.mode = mode;
9✔
119

5✔
120
    const angle = mode === 'hours' ? this.state.hourAngle : this.state.minuteAngle;
5✔
121
    const value = mode === 'hours' ? this.state.hour : this.state.minute;
122

123
    if (mode === 'hours' && this.clockType === '24h') {
124
      const hourValue = parseInt(value, 10);
3✔
125
      const isInner = hourValue === 0 || hourValue >= 13;
126
      this.renderer.setCircleSize(true);
127
      this.renderer.setCircle24hMode(isInner);
90✔
128
    } else {
129
      this.renderer.setCircleSize(true);
130
      this.renderer.setCircle24hMode(false);
6✔
131
    }
132

133
    this.renderer.setHandAngle(angle);
6✔
134
    this.renderer.setActiveValue(value);
135
  }
136

5✔
137
  setValue(mode: ClockMode, value: string): void {
138
    const angle = ClockEngine.valueToAngle(value, mode, this.clockType);
139

2✔
140
    if (mode === 'hours') {
141
      this.state.hour = value;
142
      this.state.hourAngle = angle;
35✔
143

144
      if (this.clockType === '24h') {
145
        const hourValue = parseInt(value, 10);
7✔
146
        const isInner = hourValue === 0 || hourValue >= 13;
147
        this.renderer.setCircleSize(true);
148
        this.renderer.setCircle24hMode(isInner);
149
      } else {
150
        this.renderer.setCircle24hMode(false);
151
      }
152
    } else {
153
      this.state.minute = value;
154
      this.state.minuteAngle = angle;
155
      this.renderer.setCircleSize(true);
156
      this.renderer.setCircle24hMode(false);
157
    }
158

159
    if (this.state.mode === mode) {
160
      this.renderer.setHandAngle(angle);
161
      this.renderer.setActiveValue(value);
162
    }
163
  }
164

165
  setAmPm(amPm: 'AM' | 'PM' | ''): void {
166
    this.state.amPm = amPm;
167
  }
168

169
  getState(): Readonly<ClockState> {
170
    return { ...this.state };
171
  }
172

173
  getHour(): string {
174
    return this.state.hour;
175
  }
176

177
  getMinute(): string {
178
    return this.state.minute;
179
  }
180

181
  getAmPm(): string {
182
    return this.state.amPm;
183
  }
184

185
  updateDisabledTime(disabledTime: DisabledTimeConfig | null): void {
186
    this.disabledTime = disabledTime;
187
  }
188

189
  destroy(): void {
190
    this.renderer.destroy();
191
  }
192
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc