Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [1.1.0] - 2026-02-08

### Added

- Timezone callback support via onTimezoneChange prop
- Range mode callback support via onRangeConfirm, onRangeSwitch, onRangeValidation props
- Comprehensive test suite with Vitest achieving 100% code coverage across all modules
- Plugin registration guide in README with Timezone and Range examples
- Re-exported TimezoneChangeEventData and TimepickerEventMap types from timepicker-ui

### Updated

- timepicker-ui dependency from 4.0.3 to 4.1.2

---

## [1.0.1] - 2026-01-24

### Updated
Expand Down
92 changes: 77 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function App() {
onCancel={() => console.log("Cancelled")}
onUpdate={(data) => console.log("Updated:", data)}
onSelectHour={(data) => console.log("Hour selected:", data)}
onSelectMinute={(data) => console.log("Minute selected:", data)}
onSelectMinute=(data) => console.log("Minute selected:", data)}
onSelectAM={() => console.log("AM selected")}
onSelectPM={() => console.log("PM selected")}
onError={(data) => console.log("Error:", data)}
Expand All @@ -124,6 +124,65 @@ function App() {
}
```

### Using Plugins (Timezone & Range)

> **Important:** Timezone and Range features are **plugins** that must be manually imported and registered. They are **not included by default** for tree-shaking optimization.

```tsx
"use client";

import React, { useEffect } from "react";
import { Timepicker, PluginRegistry } from "timepicker-ui-react";

function App() {
useEffect(() => {
// Register plugins once when component mounts
const registerPlugins = async () => {
const { TimezonePlugin } = await import("timepicker-ui/plugins/timezone");
const { RangePlugin } = await import("timepicker-ui/plugins/range");

PluginRegistry.register(TimezonePlugin);
PluginRegistry.register(RangePlugin);
};

registerPlugins();
}, []);

return (
<>
{/* Timezone example */}
<Timepicker
options={{
timezone: {
enabled: true,
default: "America/New_York",
},
}}
onTimezoneChange={(data) => console.log("Timezone changed:", data)}
/>

{/* Range example */}
<Timepicker
options={{
range: {
enabled: true,
},
}}
onRangeConfirm={(data) => console.log("Range confirmed:", data)}
onRangeSwitch={(data) => console.log("Range switch:", data)}
onRangeValidation={(data) => console.log("Range validation:", data)}
/>
</>
);
}
```

**Why plugins must be registered manually:**

- **Tree-shaking** - Bundle only what you use
- **Performance** - Avoid loading unnecessary code
- **SSR compatibility** - Plugins load on client-side only

### SSR (Next.js Example)

```tsx
Expand Down Expand Up @@ -152,20 +211,23 @@ The component is SSR-safe by default and will render a basic input during server

### `TimepickerProps`

| Prop | Type | Description |
| ---------------- | ------------------------------------ | ------------------------------------------ |
| `options` | `TimepickerOptions` | Full configuration from timepicker-ui core |
| `value` | `string` | Controlled value |
| `defaultValue` | `string` | Default value for uncontrolled usage |
| `onConfirm` | `CallbacksOptions['onConfirm']` | Triggered when user confirms time |
| `onCancel` | `CallbacksOptions['onCancel']` | Triggered when user cancels |
| `onOpen` | `CallbacksOptions['onOpen']` | Triggered when timepicker opens |
| `onUpdate` | `CallbacksOptions['onUpdate']` | Triggered during real-time interaction |
| `onSelectHour` | `CallbacksOptions['onSelectHour']` | Triggered when hour mode is activated |
| `onSelectMinute` | `CallbacksOptions['onSelectMinute']` | Triggered when minute mode is activated |
| `onSelectAM` | `CallbacksOptions['onSelectAM']` | Triggered when AM is selected |
| `onSelectPM` | `CallbacksOptions['onSelectPM']` | Triggered when PM is selected |
| `onError` | `CallbacksOptions['onError']` | Triggered on validation error |
| Prop | Type | Description |
| ------------------- | --------------------------------------- | ------------------------------------------ |
| `options` | `TimepickerOptions` | Full configuration from timepicker-ui core |
| `value` | `string` | Controlled value |
| `defaultValue` | `string` | Default value for uncontrolled usage |
| `onConfirm` | `CallbacksOptions['onConfirm']` | Triggered when user confirms time |
| `onCancel` | `CallbacksOptions['onCancel']` | Triggered when user cancels |
| `onOpen` | `CallbacksOptions['onOpen']` | Triggered when timepicker opens |
| `onUpdate` | `CallbacksOptions['onUpdate']` | Triggered during real-time interaction |
| `onSelectHour` | `CallbacksOptions['onSelectHour']` | Triggered when hour mode is activated |
| `onSelectMinute` | `CallbacksOptions['onSelectMinute']` | Triggered when minute mode is activated |
| `onSelectAM` | `CallbacksOptions['onSelectAM']` | Triggered when AM is selected |
| `onSelectPM` | `CallbacksOptions['onSelectPM']` | Triggered when PM is selected |
| `onTimezoneChange` | `CallbacksOptions['onTimezoneChange']` | Triggered when timezone changes (plugin) |
| `onRangeConfirm` | `CallbacksOptions['onRangeConfirm']` | Triggered when range is confirmed (plugin) |
| `onRangeSwitch` | `CallbacksOptions['onRangeSwitch']` | Triggered when range switches (plugin) |
| `onRangeValidation` | `CallbacksOptions['onRangeValidation']` | Triggered on range validation (plugin) |

The component extends `React.InputHTMLAttributes<HTMLInputElement>`, so all standard input props can be passed directly:

Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "timepicker-ui-react",
"version": "1.0.1",
"version": "1.1.0",
"description": "Official React wrapper for timepicker-ui v4.x",
"type": "module",
"sideEffects": false,
Expand All @@ -15,16 +15,16 @@
"files": [
"dist"
],
"scripts": {
"build": "cd src && npm run build"
},
"keywords": [
"time",
"timepicker",
"timepicker-ui-react",
"timepicker-ui",
"time-picker",
"react",
"react-timepicker",
"material",
"design",
"ui",
"typescript",
"material-design"
Expand All @@ -44,6 +44,6 @@
"react-dom": ">=17"
},
"dependencies": {
"timepicker-ui": "4.0.3"
"timepicker-ui": "4.1.2"
}
}
18 changes: 13 additions & 5 deletions src/Timepicker/Timepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export const Timepicker = forwardRef<HTMLInputElement, TimepickerProps>(
onSelectAM,
onSelectPM,
onError,
onTimezoneChange,
onRangeConfirm,
onRangeSwitch,
onRangeValidation,
onChange,
...inputProps
} = props;
Expand All @@ -32,7 +36,7 @@ export const Timepicker = forwardRef<HTMLInputElement, TimepickerProps>(
useImperativeHandle(
forwardedRef,
() => inputRef.current as HTMLInputElement,
[]
[],
);

const reactCallbacks = {
Expand All @@ -45,11 +49,15 @@ export const Timepicker = forwardRef<HTMLInputElement, TimepickerProps>(
onSelectAM,
onSelectPM,
onError,
onTimezoneChange,
onRangeConfirm,
onRangeSwitch,
onRangeValidation,
};

const { attachEventHandlers, detachEventHandlers } = useEventHandlers(
reactCallbacks,
options?.callbacks
options?.callbacks,
);

const pickerRef = useTimepickerInstance(
Expand All @@ -58,7 +66,7 @@ export const Timepicker = forwardRef<HTMLInputElement, TimepickerProps>(
value,
defaultValue,
attachEventHandlers,
detachEventHandlers
detachEventHandlers,
);

useTimepickerValue(pickerRef, value);
Expand All @@ -67,7 +75,7 @@ export const Timepicker = forwardRef<HTMLInputElement, TimepickerProps>(
pickerRef,
attachEventHandlers,
detachEventHandlers,
reactCallbacks
reactCallbacks,
);

const isControlled = value !== undefined;
Expand All @@ -80,7 +88,7 @@ export const Timepicker = forwardRef<HTMLInputElement, TimepickerProps>(
{...(isControlled ? { value, readOnly: true } : { defaultValue })}
/>
);
}
},
);

Timepicker.displayName = "Timepicker";
71 changes: 71 additions & 0 deletions src/Timepicker/__tests__/Timepicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect, afterEach } from "vitest";
import React, { createRef } from "react";
import { render, screen, cleanup, act } from "@testing-library/react";
import { Timepicker } from "../Timepicker";

afterEach(cleanup);

describe("Timepicker", () => {
it("renders an input element", async () => {
await act(async () => {
render(<Timepicker placeholder="Select time" />);
});

const input = screen.getByPlaceholderText("Select time");
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute("type", "text");
});

it("has displayName set", () => {
expect(Timepicker.displayName).toBe("Timepicker");
});

it("forwards ref to the input element", async () => {
const ref = createRef<HTMLInputElement>();

await act(async () => {
render(<Timepicker ref={ref} placeholder="ref-test" />);
});

expect(ref.current).toBeInstanceOf(HTMLInputElement);
expect(ref.current?.placeholder).toBe("ref-test");
});

it("spreads native input attributes", async () => {
await act(async () => {
render(
<Timepicker
placeholder="test"
className="my-class"
disabled
id="time-input"
/>,
);
});

const input = screen.getByPlaceholderText("test");
expect(input).toHaveClass("my-class");
expect(input).toBeDisabled();
expect(input).toHaveAttribute("id", "time-input");
});

it("renders as uncontrolled with defaultValue", async () => {
await act(async () => {
render(<Timepicker defaultValue="09:30" placeholder="uncontrolled" />);
});

const input = screen.getByPlaceholderText("uncontrolled");
expect(input).toHaveAttribute("value", "09:30");
expect(input).not.toHaveAttribute("readonly");
});

it("renders as controlled with value and readOnly", async () => {
await act(async () => {
render(<Timepicker value="14:00" placeholder="controlled" />);
});

const input = screen.getByPlaceholderText("controlled");
expect(input).toHaveAttribute("value", "14:00");
expect(input).toHaveAttribute("readonly");
});
});
Loading
Loading