@react-chess-tools/react-chess-clock 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +697 -0
- package/dist/index.cjs +1014 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +528 -0
- package/dist/index.d.ts +528 -0
- package/dist/index.js +969 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/components/ChessClock/ChessClock.stories.tsx +782 -0
- package/src/components/ChessClock/index.ts +44 -0
- package/src/components/ChessClock/parts/Display.tsx +69 -0
- package/src/components/ChessClock/parts/PlayPause.tsx +190 -0
- package/src/components/ChessClock/parts/Reset.tsx +90 -0
- package/src/components/ChessClock/parts/Root.tsx +37 -0
- package/src/components/ChessClock/parts/Switch.tsx +84 -0
- package/src/components/ChessClock/parts/__tests__/Display.test.tsx +149 -0
- package/src/components/ChessClock/parts/__tests__/PlayPause.test.tsx +411 -0
- package/src/components/ChessClock/parts/__tests__/Reset.test.tsx +160 -0
- package/src/components/ChessClock/parts/__tests__/Root.test.tsx +49 -0
- package/src/components/ChessClock/parts/__tests__/Switch.test.tsx +204 -0
- package/src/hooks/__tests__/clockReducer.test.ts +985 -0
- package/src/hooks/__tests__/useChessClock.test.tsx +1080 -0
- package/src/hooks/clockReducer.ts +379 -0
- package/src/hooks/useChessClock.ts +406 -0
- package/src/hooks/useChessClockContext.ts +35 -0
- package/src/index.ts +65 -0
- package/src/types.ts +217 -0
- package/src/utils/__tests__/calculateSwitchTime.test.ts +150 -0
- package/src/utils/__tests__/formatTime.test.ts +83 -0
- package/src/utils/__tests__/timeControl.test.ts +414 -0
- package/src/utils/__tests__/timingMethods.test.ts +170 -0
- package/src/utils/calculateSwitchTime.ts +37 -0
- package/src/utils/formatTime.ts +59 -0
- package/src/utils/presets.ts +47 -0
- package/src/utils/timeControl.ts +205 -0
- package/src/utils/timingMethods.ts +103 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Root } from "./parts/Root";
|
|
2
|
+
import { Display } from "./parts/Display";
|
|
3
|
+
import { Switch } from "./parts/Switch";
|
|
4
|
+
import { PlayPause } from "./parts/PlayPause";
|
|
5
|
+
import { Reset } from "./parts/Reset";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ChessClock compound components
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { ChessClock } from "@react-chess-tools/react-chess-clock";
|
|
13
|
+
*
|
|
14
|
+
* function ClockApp() {
|
|
15
|
+
* return (
|
|
16
|
+
* <ChessClock.Root timeControl={{ time: "5+3" }}>
|
|
17
|
+
* <ChessClock.Display color="black" />
|
|
18
|
+
* <ChessClock.Display color="white" />
|
|
19
|
+
* <ChessClock.Switch>Switch</ChessClock.Switch>
|
|
20
|
+
* <ChessClock.PlayPause
|
|
21
|
+
* startContent="Start"
|
|
22
|
+
* pauseContent="Pause"
|
|
23
|
+
* resumeContent="Resume"
|
|
24
|
+
* />
|
|
25
|
+
* <ChessClock.Reset>Reset</ChessClock.Reset>
|
|
26
|
+
* </ChessClock.Root>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const ChessClock = {
|
|
32
|
+
Root,
|
|
33
|
+
Display,
|
|
34
|
+
Switch,
|
|
35
|
+
PlayPause,
|
|
36
|
+
Reset,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Re-export types for convenience
|
|
40
|
+
export type { ChessClockRootProps } from "./parts/Root";
|
|
41
|
+
export type { ChessClockDisplayProps } from "./parts/Display";
|
|
42
|
+
export type { ChessClockControlProps } from "./parts/Switch";
|
|
43
|
+
export type { ChessClockPlayPauseProps } from "./parts/PlayPause";
|
|
44
|
+
export type { ChessClockResetProps } from "./parts/Reset";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ClockColor } from "../../../types";
|
|
3
|
+
import { useChessClockContext } from "../../../hooks/useChessClockContext";
|
|
4
|
+
import { formatClockTime } from "../../../utils/formatTime";
|
|
5
|
+
|
|
6
|
+
export interface ChessClockDisplayProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
color: ClockColor;
|
|
8
|
+
format?: "auto" | "mm:ss" | "ss.d" | "hh:mm:ss";
|
|
9
|
+
formatTime?: (milliseconds: number) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ChessClock.Display - Displays the current time for a player
|
|
14
|
+
*
|
|
15
|
+
* Renders an unstyled div with data attributes for custom styling.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <ChessClock.Display color="white" format="auto" />
|
|
20
|
+
* <ChessClock.Display color="black" format="ss.d" />
|
|
21
|
+
* <ChessClock.Display
|
|
22
|
+
* color="white"
|
|
23
|
+
* formatTime={(ms) => `${Math.ceil(ms / 1000)}s`}
|
|
24
|
+
* />
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const Display = React.forwardRef<HTMLDivElement, ChessClockDisplayProps>(
|
|
28
|
+
(
|
|
29
|
+
{
|
|
30
|
+
color,
|
|
31
|
+
format = "auto",
|
|
32
|
+
formatTime: customFormatTime,
|
|
33
|
+
className,
|
|
34
|
+
style,
|
|
35
|
+
...rest
|
|
36
|
+
},
|
|
37
|
+
ref,
|
|
38
|
+
) => {
|
|
39
|
+
const { times, activePlayer, status, timeout } = useChessClockContext();
|
|
40
|
+
|
|
41
|
+
const time = times[color];
|
|
42
|
+
const isActive = activePlayer === color;
|
|
43
|
+
const hasTimeout = timeout === color;
|
|
44
|
+
const isPaused = status === "paused";
|
|
45
|
+
|
|
46
|
+
// Format the time for display
|
|
47
|
+
const formattedTime = customFormatTime
|
|
48
|
+
? customFormatTime(time)
|
|
49
|
+
: formatClockTime(time, format);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
ref={ref}
|
|
54
|
+
className={className}
|
|
55
|
+
style={style}
|
|
56
|
+
data-clock-color={color}
|
|
57
|
+
data-clock-active={isActive ? "true" : "false"}
|
|
58
|
+
data-clock-paused={isPaused ? "true" : "false"}
|
|
59
|
+
data-clock-timeout={hasTimeout ? "true" : "false"}
|
|
60
|
+
data-clock-status={status}
|
|
61
|
+
{...rest}
|
|
62
|
+
>
|
|
63
|
+
{formattedTime}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
Display.displayName = "ChessClock.Display";
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { useChessClockContext } from "../../../hooks/useChessClockContext";
|
|
5
|
+
|
|
6
|
+
// Default content for each clock state
|
|
7
|
+
const DEFAULT_CONTENT = {
|
|
8
|
+
start: "Start",
|
|
9
|
+
pause: "Pause",
|
|
10
|
+
resume: "Resume",
|
|
11
|
+
delayed: "Start",
|
|
12
|
+
finished: "Game Over",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the appropriate content to display based on clock state
|
|
17
|
+
* and custom content props. Custom content takes precedence over defaults.
|
|
18
|
+
*/
|
|
19
|
+
const resolveContent = (
|
|
20
|
+
isFinished: boolean,
|
|
21
|
+
isDelayed: boolean,
|
|
22
|
+
shouldShowStart: boolean,
|
|
23
|
+
isPaused: boolean,
|
|
24
|
+
isRunning: boolean,
|
|
25
|
+
customContent: {
|
|
26
|
+
startContent?: ReactNode;
|
|
27
|
+
pauseContent?: ReactNode;
|
|
28
|
+
resumeContent?: ReactNode;
|
|
29
|
+
delayedContent?: ReactNode;
|
|
30
|
+
finishedContent?: ReactNode;
|
|
31
|
+
},
|
|
32
|
+
): ReactNode => {
|
|
33
|
+
if (isFinished) {
|
|
34
|
+
return customContent.finishedContent ?? DEFAULT_CONTENT.finished;
|
|
35
|
+
}
|
|
36
|
+
if (isDelayed) {
|
|
37
|
+
return customContent.delayedContent ?? DEFAULT_CONTENT.delayed;
|
|
38
|
+
}
|
|
39
|
+
if (shouldShowStart) {
|
|
40
|
+
return customContent.startContent ?? DEFAULT_CONTENT.start;
|
|
41
|
+
}
|
|
42
|
+
if (isPaused) {
|
|
43
|
+
return customContent.resumeContent ?? DEFAULT_CONTENT.resume;
|
|
44
|
+
}
|
|
45
|
+
if (isRunning) {
|
|
46
|
+
return customContent.pauseContent ?? DEFAULT_CONTENT.pause;
|
|
47
|
+
}
|
|
48
|
+
return DEFAULT_CONTENT.start;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface ChessClockPlayPauseProps extends Omit<
|
|
52
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
53
|
+
"onClick"
|
|
54
|
+
> {
|
|
55
|
+
asChild?: boolean;
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
58
|
+
/** Content shown when clock is idle (not yet started) - clicking will start the clock */
|
|
59
|
+
startContent?: ReactNode;
|
|
60
|
+
/** Content shown when clock is running - clicking will pause */
|
|
61
|
+
pauseContent?: ReactNode;
|
|
62
|
+
/** Content shown when clock is paused - clicking will resume */
|
|
63
|
+
resumeContent?: ReactNode;
|
|
64
|
+
/** Content shown when clock is in delayed mode - clicking will start the clock immediately */
|
|
65
|
+
delayedContent?: ReactNode;
|
|
66
|
+
/** Content shown when clock is finished - button is disabled but shows this content */
|
|
67
|
+
finishedContent?: ReactNode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* ChessClock.PlayPause - Button to start, pause, and resume the clock
|
|
72
|
+
*
|
|
73
|
+
* Supports the asChild pattern and conditional content based on clock state.
|
|
74
|
+
* When no children or custom content is provided, sensible defaults are used for each state.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* // With children (backward compatible)
|
|
79
|
+
* <ChessClock.PlayPause>
|
|
80
|
+
* <span>Toggle</span>
|
|
81
|
+
* </ChessClock.PlayPause>
|
|
82
|
+
*
|
|
83
|
+
* // No props - uses defaults for all states
|
|
84
|
+
* <ChessClock.PlayPause />
|
|
85
|
+
* // Shows: "Start" → "Pause" → "Resume" → "Game Over"
|
|
86
|
+
*
|
|
87
|
+
* // Override just one state, others use defaults
|
|
88
|
+
* <ChessClock.PlayPause pauseContent="⏸️ Stop" />
|
|
89
|
+
* // Shows: "Start" → "⏸️ Stop" → "Resume" → "Game Over"
|
|
90
|
+
*
|
|
91
|
+
* // As child
|
|
92
|
+
* <ChessClock.PlayPause asChild>
|
|
93
|
+
* <div className="custom-button">Toggle</div>
|
|
94
|
+
* </ChessClock.PlayPause>
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export const PlayPause = React.forwardRef<
|
|
98
|
+
HTMLElement,
|
|
99
|
+
React.PropsWithChildren<ChessClockPlayPauseProps>
|
|
100
|
+
>(
|
|
101
|
+
(
|
|
102
|
+
{
|
|
103
|
+
asChild = false,
|
|
104
|
+
startContent,
|
|
105
|
+
pauseContent,
|
|
106
|
+
resumeContent,
|
|
107
|
+
delayedContent,
|
|
108
|
+
finishedContent,
|
|
109
|
+
children,
|
|
110
|
+
onClick,
|
|
111
|
+
disabled,
|
|
112
|
+
className,
|
|
113
|
+
style,
|
|
114
|
+
type,
|
|
115
|
+
...rest
|
|
116
|
+
},
|
|
117
|
+
ref,
|
|
118
|
+
) => {
|
|
119
|
+
const { status, methods } = useChessClockContext();
|
|
120
|
+
const isIdle = status === "idle";
|
|
121
|
+
const isDelayed = status === "delayed";
|
|
122
|
+
const isPaused = status === "paused";
|
|
123
|
+
const isRunning = status === "running";
|
|
124
|
+
const isFinished = status === "finished";
|
|
125
|
+
|
|
126
|
+
// Treat "delayed" like "idle" - clock hasn't started yet
|
|
127
|
+
const shouldShowStart = isIdle || isDelayed;
|
|
128
|
+
|
|
129
|
+
const isDisabled = disabled || isFinished || isDelayed;
|
|
130
|
+
|
|
131
|
+
const handleClick = React.useCallback(
|
|
132
|
+
(e: React.MouseEvent<HTMLElement>) => {
|
|
133
|
+
if (shouldShowStart) {
|
|
134
|
+
methods.start();
|
|
135
|
+
} else if (isPaused) {
|
|
136
|
+
methods.resume();
|
|
137
|
+
} else if (isRunning) {
|
|
138
|
+
methods.pause();
|
|
139
|
+
}
|
|
140
|
+
onClick?.(e);
|
|
141
|
+
},
|
|
142
|
+
[shouldShowStart, isPaused, isRunning, methods, onClick],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Determine content to render
|
|
146
|
+
// Priority: children > custom+defaults > all defaults
|
|
147
|
+
const content =
|
|
148
|
+
children ??
|
|
149
|
+
resolveContent(
|
|
150
|
+
isFinished,
|
|
151
|
+
isDelayed,
|
|
152
|
+
shouldShowStart,
|
|
153
|
+
isPaused,
|
|
154
|
+
isRunning,
|
|
155
|
+
{
|
|
156
|
+
startContent,
|
|
157
|
+
pauseContent,
|
|
158
|
+
resumeContent,
|
|
159
|
+
delayedContent,
|
|
160
|
+
finishedContent,
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return asChild ? (
|
|
165
|
+
<Slot
|
|
166
|
+
ref={ref}
|
|
167
|
+
onClick={handleClick}
|
|
168
|
+
className={className}
|
|
169
|
+
style={style}
|
|
170
|
+
{...{ ...rest, disabled: isDisabled }}
|
|
171
|
+
>
|
|
172
|
+
{content}
|
|
173
|
+
</Slot>
|
|
174
|
+
) : (
|
|
175
|
+
<button
|
|
176
|
+
ref={ref as React.RefObject<HTMLButtonElement>}
|
|
177
|
+
type={type || "button"}
|
|
178
|
+
className={className}
|
|
179
|
+
style={style}
|
|
180
|
+
onClick={handleClick}
|
|
181
|
+
disabled={isDisabled}
|
|
182
|
+
{...rest}
|
|
183
|
+
>
|
|
184
|
+
{content}
|
|
185
|
+
</button>
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
PlayPause.displayName = "ChessClock.PlayPause";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { useChessClockContext } from "../../../hooks/useChessClockContext";
|
|
5
|
+
import type { TimeControlInput } from "../../../types";
|
|
6
|
+
|
|
7
|
+
export interface ChessClockResetProps extends Omit<
|
|
8
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
9
|
+
"onClick"
|
|
10
|
+
> {
|
|
11
|
+
asChild?: boolean;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
14
|
+
timeControl?: TimeControlInput;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ChessClock.Reset - Button to reset the clock
|
|
19
|
+
*
|
|
20
|
+
* Supports the asChild pattern and optional new time control on reset.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <ChessClock.Reset>Reset</ChessClock.Reset>
|
|
25
|
+
*
|
|
26
|
+
* // Reset with new time control
|
|
27
|
+
* <ChessClock.Reset timeControl="10+5">Change to 10+5</ChessClock.Reset>
|
|
28
|
+
*
|
|
29
|
+
* // As child
|
|
30
|
+
* <ChessClock.Reset asChild>
|
|
31
|
+
* <div className="custom-reset">Reset</div>
|
|
32
|
+
* </ChessClock.Reset>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const Reset = React.forwardRef<
|
|
36
|
+
HTMLElement,
|
|
37
|
+
React.PropsWithChildren<ChessClockResetProps>
|
|
38
|
+
>(
|
|
39
|
+
(
|
|
40
|
+
{
|
|
41
|
+
asChild = false,
|
|
42
|
+
timeControl,
|
|
43
|
+
children,
|
|
44
|
+
onClick,
|
|
45
|
+
disabled,
|
|
46
|
+
className,
|
|
47
|
+
style,
|
|
48
|
+
type,
|
|
49
|
+
...rest
|
|
50
|
+
},
|
|
51
|
+
ref,
|
|
52
|
+
) => {
|
|
53
|
+
const { methods, status } = useChessClockContext();
|
|
54
|
+
const isDisabled = disabled || status === "idle";
|
|
55
|
+
|
|
56
|
+
const handleClick = React.useCallback(
|
|
57
|
+
(e: React.MouseEvent<HTMLElement>) => {
|
|
58
|
+
methods.reset(timeControl);
|
|
59
|
+
onClick?.(e);
|
|
60
|
+
},
|
|
61
|
+
[methods, timeControl, onClick],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return asChild ? (
|
|
65
|
+
<Slot
|
|
66
|
+
ref={ref}
|
|
67
|
+
onClick={handleClick}
|
|
68
|
+
className={className}
|
|
69
|
+
style={style}
|
|
70
|
+
{...{ ...rest, disabled: isDisabled }}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</Slot>
|
|
74
|
+
) : (
|
|
75
|
+
<button
|
|
76
|
+
ref={ref as React.RefObject<HTMLButtonElement>}
|
|
77
|
+
type={type || "button"}
|
|
78
|
+
className={className}
|
|
79
|
+
style={style}
|
|
80
|
+
onClick={handleClick}
|
|
81
|
+
disabled={isDisabled}
|
|
82
|
+
{...rest}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</button>
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
Reset.displayName = "ChessClock.Reset";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { ChessClockContext } from "../../../hooks/useChessClockContext";
|
|
4
|
+
import { useChessClock } from "../../../hooks/useChessClock";
|
|
5
|
+
import type { TimeControlConfig } from "../../../types";
|
|
6
|
+
|
|
7
|
+
export interface ChessClockRootProps {
|
|
8
|
+
timeControl: TimeControlConfig;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ChessClock.Root - Context provider for chess clock components
|
|
14
|
+
* Manages clock state and provides it to child components
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <ChessClock.Root timeControl={{ time: "5+3" }}>
|
|
19
|
+
* <ChessClock.Display color="white" />
|
|
20
|
+
* <ChessClock.Display color="black" />
|
|
21
|
+
* </ChessClock.Root>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const Root: React.FC<React.PropsWithChildren<ChessClockRootProps>> = ({
|
|
25
|
+
timeControl,
|
|
26
|
+
children,
|
|
27
|
+
}) => {
|
|
28
|
+
const clockState = useChessClock(timeControl);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<ChessClockContext.Provider value={clockState}>
|
|
32
|
+
{children}
|
|
33
|
+
</ChessClockContext.Provider>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
Root.displayName = "ChessClock.Root";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { useChessClockContext } from "../../../hooks/useChessClockContext";
|
|
5
|
+
|
|
6
|
+
export interface ChessClockControlProps extends Omit<
|
|
7
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
8
|
+
"onClick"
|
|
9
|
+
> {
|
|
10
|
+
asChild?: boolean;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ChessClock.Switch - Button to manually switch the active clock
|
|
17
|
+
*
|
|
18
|
+
* Supports the asChild pattern for custom rendering.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <ChessClock.Switch>Switch Clock</ChessClock.Switch>
|
|
23
|
+
*
|
|
24
|
+
* // As child
|
|
25
|
+
* <ChessClock.Switch asChild>
|
|
26
|
+
* <div className="custom-switch">Switch</div>
|
|
27
|
+
* </ChessClock.Switch>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export const Switch = React.forwardRef<
|
|
31
|
+
HTMLElement,
|
|
32
|
+
React.PropsWithChildren<ChessClockControlProps>
|
|
33
|
+
>(
|
|
34
|
+
(
|
|
35
|
+
{
|
|
36
|
+
asChild = false,
|
|
37
|
+
children,
|
|
38
|
+
onClick,
|
|
39
|
+
disabled,
|
|
40
|
+
className,
|
|
41
|
+
style,
|
|
42
|
+
type,
|
|
43
|
+
...rest
|
|
44
|
+
},
|
|
45
|
+
ref,
|
|
46
|
+
) => {
|
|
47
|
+
const { methods, status } = useChessClockContext();
|
|
48
|
+
const isDisabled = disabled || status === "finished" || status === "idle";
|
|
49
|
+
|
|
50
|
+
const handleClick = React.useCallback(
|
|
51
|
+
(e: React.MouseEvent<HTMLElement>) => {
|
|
52
|
+
methods.switch();
|
|
53
|
+
onClick?.(e);
|
|
54
|
+
},
|
|
55
|
+
[methods, onClick],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return asChild ? (
|
|
59
|
+
<Slot
|
|
60
|
+
ref={ref}
|
|
61
|
+
onClick={handleClick}
|
|
62
|
+
className={className}
|
|
63
|
+
style={style}
|
|
64
|
+
{...{ ...rest, disabled: isDisabled }}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</Slot>
|
|
68
|
+
) : (
|
|
69
|
+
<button
|
|
70
|
+
ref={ref as React.RefObject<HTMLButtonElement>}
|
|
71
|
+
type={type || "button"}
|
|
72
|
+
className={className}
|
|
73
|
+
style={style}
|
|
74
|
+
onClick={handleClick}
|
|
75
|
+
disabled={isDisabled}
|
|
76
|
+
{...rest}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</button>
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
Switch.displayName = "ChessClock.Switch";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { ChessClock } from "../../index";
|
|
4
|
+
|
|
5
|
+
describe("ChessClock.Display", () => {
|
|
6
|
+
it("should render white player time", () => {
|
|
7
|
+
render(
|
|
8
|
+
<ChessClock.Root timeControl={{ time: "5+0" }}>
|
|
9
|
+
<ChessClock.Display color="white" data-testid="white-clock" />
|
|
10
|
+
</ChessClock.Root>,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const display = screen.getByTestId("white-clock");
|
|
14
|
+
expect(display).toBeInTheDocument();
|
|
15
|
+
expect(display).toHaveTextContent("5:00");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should render black player time", () => {
|
|
19
|
+
render(
|
|
20
|
+
<ChessClock.Root timeControl={{ time: "10+5" }}>
|
|
21
|
+
<ChessClock.Display color="black" data-testid="black-clock" />
|
|
22
|
+
</ChessClock.Root>,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const display = screen.getByTestId("black-clock");
|
|
26
|
+
expect(display).toHaveTextContent("10:00");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should include data attributes", () => {
|
|
30
|
+
render(
|
|
31
|
+
<ChessClock.Root timeControl={{ time: "5+0" }}>
|
|
32
|
+
<ChessClock.Display color="white" data-testid="clock" />
|
|
33
|
+
</ChessClock.Root>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const display = screen.getByTestId("clock");
|
|
37
|
+
expect(display).toHaveAttribute("data-clock-color", "white");
|
|
38
|
+
expect(display).toHaveAttribute("data-clock-active", "false");
|
|
39
|
+
expect(display).toHaveAttribute("data-clock-paused", "false");
|
|
40
|
+
expect(display).toHaveAttribute("data-clock-timeout", "false");
|
|
41
|
+
expect(display).toHaveAttribute("data-clock-status", "delayed"); // default clockStart is "delayed"
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should show active clock when clock is running", () => {
|
|
45
|
+
render(
|
|
46
|
+
<ChessClock.Root timeControl={{ time: "5+0", clockStart: "immediate" }}>
|
|
47
|
+
<ChessClock.Display color="white" data-testid="white-clock" />
|
|
48
|
+
<ChessClock.Display color="black" data-testid="black-clock" />
|
|
49
|
+
</ChessClock.Root>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const whiteClock = screen.getByTestId("white-clock");
|
|
53
|
+
const blackClock = screen.getByTestId("black-clock");
|
|
54
|
+
|
|
55
|
+
expect(whiteClock).toHaveAttribute("data-clock-active", "true");
|
|
56
|
+
expect(blackClock).toHaveAttribute("data-clock-active", "false");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should support custom className", () => {
|
|
60
|
+
render(
|
|
61
|
+
<ChessClock.Root timeControl={{ time: "5+0" }}>
|
|
62
|
+
<ChessClock.Display
|
|
63
|
+
color="white"
|
|
64
|
+
className="custom-class"
|
|
65
|
+
data-testid="clock"
|
|
66
|
+
/>
|
|
67
|
+
</ChessClock.Root>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByTestId("clock")).toHaveClass("custom-class");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should support custom style", () => {
|
|
74
|
+
render(
|
|
75
|
+
<ChessClock.Root timeControl={{ time: "5+0" }}>
|
|
76
|
+
<ChessClock.Display
|
|
77
|
+
color="white"
|
|
78
|
+
style={{ fontSize: "24px" }}
|
|
79
|
+
data-testid="clock"
|
|
80
|
+
/>
|
|
81
|
+
</ChessClock.Root>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(screen.getByTestId("clock")).toHaveStyle({ fontSize: "24px" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("format prop", () => {
|
|
88
|
+
it("should format as mm:ss", () => {
|
|
89
|
+
render(
|
|
90
|
+
<ChessClock.Root timeControl={{ time: "5+0" }}>
|
|
91
|
+
<ChessClock.Display
|
|
92
|
+
color="white"
|
|
93
|
+
format="mm:ss"
|
|
94
|
+
data-testid="clock"
|
|
95
|
+
/>
|
|
96
|
+
</ChessClock.Root>,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(screen.getByTestId("clock")).toHaveTextContent("5:00");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should format as ss.d", () => {
|
|
103
|
+
render(
|
|
104
|
+
<ChessClock.Root timeControl={{ time: "5+0" }}>
|
|
105
|
+
<ChessClock.Display color="white" format="ss.d" data-testid="clock" />
|
|
106
|
+
</ChessClock.Root>,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(screen.getByTestId("clock")).toHaveTextContent("300.0");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should format as hh:mm:ss for long times", () => {
|
|
113
|
+
render(
|
|
114
|
+
<ChessClock.Root timeControl={{ time: "90+30" }}>
|
|
115
|
+
<ChessClock.Display
|
|
116
|
+
color="white"
|
|
117
|
+
format="hh:mm:ss"
|
|
118
|
+
data-testid="clock"
|
|
119
|
+
/>
|
|
120
|
+
</ChessClock.Root>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(screen.getByTestId("clock")).toHaveTextContent("1:30:00");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("formatTime prop", () => {
|
|
128
|
+
it("should use custom formatTime function", () => {
|
|
129
|
+
const customFormat = jest.fn((ms) => `${Math.ceil(ms / 1000)}s`);
|
|
130
|
+
|
|
131
|
+
render(
|
|
132
|
+
<ChessClock.Root timeControl={{ time: "5+0" }}>
|
|
133
|
+
<ChessClock.Display
|
|
134
|
+
color="white"
|
|
135
|
+
formatTime={customFormat}
|
|
136
|
+
data-testid="clock"
|
|
137
|
+
/>
|
|
138
|
+
</ChessClock.Root>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(screen.getByTestId("clock")).toHaveTextContent("300s");
|
|
142
|
+
expect(customFormat).toHaveBeenCalledWith(300_000);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should have displayName", () => {
|
|
147
|
+
expect(ChessClock.Display.displayName).toBe("ChessClock.Display");
|
|
148
|
+
});
|
|
149
|
+
});
|