@daxence/flexy-date-picker 1.0.0
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/LICENSE +21 -0
- package/README.md +515 -0
- package/dist/flexy-date-picker.js +3764 -0
- package/dist/flexy-date-picker.js.map +1 -0
- package/dist/flexy-date-picker.umd.cjs +5 -0
- package/dist/flexy-date-picker.umd.cjs.map +1 -0
- package/dist/style.css +1 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +118 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daxence Solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
# flexy-date-picker
|
|
2
|
+
|
|
3
|
+
[](https://github.com/daxence/flexy-date-picker/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/daxence/flexy-date-picker/actions/workflows/release.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@daxence/flexy-date-picker)
|
|
6
|
+
[](https://www.npmjs.com/package/@daxence/flexy-date-picker)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://react.dev)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](#quality--testing)
|
|
11
|
+
[](https://www.conventionalcommits.org)
|
|
12
|
+
[](./CONTRIBUTING.md)
|
|
13
|
+
|
|
14
|
+
A **fully customizable** React date & time picker with single / range selection, an optional time picker, injectable CSS theming, tooltips, and a complete event API. Built with TypeScript, Day.js, and zero hard-coded visual opinions — every color, radius, and slot can be overridden.
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
- [Features](#features)
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Quick start](#quick-start)
|
|
24
|
+
- [Live examples](#live-examples)
|
|
25
|
+
- [Components](#components)
|
|
26
|
+
- [`DatePicker`](#datepicker)
|
|
27
|
+
- [`TimePicker`](#timepicker-standalone)
|
|
28
|
+
- [`CalendarLegend`](#calendarlegend)
|
|
29
|
+
- [Theming via CSS custom properties](#theming-via-css-custom-properties)
|
|
30
|
+
- [Per-slot class / style injection](#per-slot-class--style-injection)
|
|
31
|
+
- [Tooltips](#tooltips)
|
|
32
|
+
- [Disabled dates](#disabled-dates)
|
|
33
|
+
- [Date + time picker](#date--time-picker)
|
|
34
|
+
- [Per-date time schedules](#per-date-time-schedules)
|
|
35
|
+
- [Range picker](#range-picker)
|
|
36
|
+
- [Custom renderers](#custom-renderers)
|
|
37
|
+
- [Accessibility](#accessibility)
|
|
38
|
+
- [Quality & testing](#quality--testing)
|
|
39
|
+
- [Development](#development)
|
|
40
|
+
- [Contributing](#contributing)
|
|
41
|
+
- [License](#license)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- ⚛️ React 19 + TypeScript, works with any React ≥ 18 host app
|
|
48
|
+
- 📅 [Day.js](https://day.js.org/) under the hood — no moment, no deprecated libs
|
|
49
|
+
- 🔀 Single-date and date-range selection modes
|
|
50
|
+
- ⏰ Optional built-in time picker (uniform step, custom ranges, or full per-date schedules) and a fully standalone `TimePicker` component
|
|
51
|
+
- 🎨 CSS custom properties — inject any color/shape/spacing without a build step
|
|
52
|
+
- 🧩 Per-slot `classNames` and `styles` props for surgical overrides (30+ slots)
|
|
53
|
+
- 🌍 Locale & RTL-friendly via dayjs locales (`fr`, `ar`, …) and configurable week start
|
|
54
|
+
- 📣 Full event API: `onBeforeSelect`, `onAfterSelect`, `onDateChange`, `onRangeChange`, `onTimeChange`, `onMonthChange`, `onYearChange`, `onDayHover`, `onOpen`, `onClose`, `onClear`
|
|
55
|
+
- 💬 Tooltip support (string, `ReactNode`, or render-prop with rich per-day context)
|
|
56
|
+
- 🖌️ Custom renderers for days, nav buttons, and the input trigger
|
|
57
|
+
- 🧱 `inline` mode to embed the calendar directly in a layout (no popover)
|
|
58
|
+
- ♿ Accessible by default: `role="grid"`, `aria-pressed`, `aria-disabled`, `aria-label`, keyboard-navigable listboxes
|
|
59
|
+
- 🚀 Vite library mode — ESM + UMD builds, tree-shakeable, fully typed
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @daxence/flexy-date-picker dayjs
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm add @daxence/flexy-date-picker dayjs
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`dayjs` is a peer dependency and must be installed alongside the package. `react` and `react-dom` (≥ 18) are also required as peer dependencies.
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { DatePicker } from '@daxence/flexy-date-picker';
|
|
79
|
+
import '@daxence/flexy-date-picker/styles'; // import the CSS once, anywhere in your app
|
|
80
|
+
|
|
81
|
+
function App() {
|
|
82
|
+
return <DatePicker mode="single" onDateChange={(date) => console.log(date)} />;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Live examples
|
|
87
|
+
|
|
88
|
+
- **Demo app** — run `npm run dev` for a playground covering every prop combination (booking calendars, day content injection, multilingual pickers, time schedules, custom themes, and more). Each demo section includes a "View code" toggle with the exact snippet used.
|
|
89
|
+
- **Storybook** — run `npm run storybook` for isolated, documented stories per component (`DatePicker`, `TimePicker`, `CalendarLegend`), including controls for every prop.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Components
|
|
94
|
+
|
|
95
|
+
### `DatePicker`
|
|
96
|
+
|
|
97
|
+
The main component. Supports single-date and range selection, an optional time picker, and full styling/theming control.
|
|
98
|
+
|
|
99
|
+
#### Selection & value props
|
|
100
|
+
|
|
101
|
+
| Prop | Type | Default | Description |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| `mode` | `'single' \| 'range'` | `'single'` | Selection mode |
|
|
104
|
+
| `value` | `Dayjs \| null` | — | Controlled single value |
|
|
105
|
+
| `defaultValue` | `Dayjs \| null` | — | Uncontrolled default |
|
|
106
|
+
| `rangeValue` | `{ start, end }` | — | Controlled range value |
|
|
107
|
+
| `defaultRangeValue` | `{ start, end }` | — | Uncontrolled range default |
|
|
108
|
+
|
|
109
|
+
#### Formatting & locale
|
|
110
|
+
|
|
111
|
+
| Prop | Type | Default | Description |
|
|
112
|
+
|---|---|---|---|
|
|
113
|
+
| `format` | `string` | `'MMM D, YYYY'` | dayjs format string |
|
|
114
|
+
| `locale` | `string` | — | dayjs locale (e.g. `'fr'`, `'ar'`) |
|
|
115
|
+
| `lang` | `string` | — | Alias for `locale` |
|
|
116
|
+
| `weekStartsOn` | `0–6` | `0` (Sunday) | First day of week |
|
|
117
|
+
|
|
118
|
+
#### Calendar display
|
|
119
|
+
|
|
120
|
+
| Prop | Type | Default | Description |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| `numberOfMonths` | `number` | `1` | Months displayed at once |
|
|
123
|
+
| `showMonthYearSelectors` | `boolean` | `true` | Show month/year dropdown selectors |
|
|
124
|
+
| `showAdjacentMonthDays` | `boolean` | `false` | Show adjacent-month days in the grid |
|
|
125
|
+
| `showOutsideDays` | `boolean` | — | Deprecated alias for `showAdjacentMonthDays` |
|
|
126
|
+
| `highlightToday` | `boolean` | `true` | Highlight today |
|
|
127
|
+
| `inline` | `boolean` | `false` | Render the calendar directly, without an input/popover |
|
|
128
|
+
| `placement` | `'top' \| 'bottom' \| 'auto'` | `'auto'` | Popover placement |
|
|
129
|
+
|
|
130
|
+
#### Constraints & state
|
|
131
|
+
|
|
132
|
+
| Prop | Type | Default | Description |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| `minDate` / `maxDate` | `Dayjs` | — | Earliest / latest selectable date |
|
|
135
|
+
| `disabled` | `DisabledConfig \| boolean` | — | Disable dates (`before`, `after`, `dates`, `predicate`) |
|
|
136
|
+
| `isDisabled` | `boolean` | `false` | Disable the whole picker |
|
|
137
|
+
| `readOnly` | `boolean` | `false` | Read-only mode |
|
|
138
|
+
| `placeholder` | `string \| [string, string]` | — | Input placeholder(s) |
|
|
139
|
+
| `clearable` | `boolean` | `true` | Show the clear button |
|
|
140
|
+
|
|
141
|
+
#### Time picker (built into `DatePicker`)
|
|
142
|
+
|
|
143
|
+
| Prop | Type | Default | Description |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| `enableTime` | `boolean` | `false` | Show a time slot list alongside the calendar |
|
|
146
|
+
| `timeStep` | `number` | `15` | Uniform slot interval in minutes |
|
|
147
|
+
| `timeFormat` | `string` | `'HH:mm'` | dayjs format for time labels |
|
|
148
|
+
| `minTime` / `maxTime` | `string` (`HH:mm`) | `'00:00'` / `'23:59'` | Bounds for generated slots |
|
|
149
|
+
| `disabledTime` | `DisabledTimeConfig` | — | Disable slots by weekday, date, or predicate |
|
|
150
|
+
| `timeSchedule` | `Record<'YYYY-MM-DD', DateSchedule>` | — | Per-date schedule override (multiple ranges, custom steps, unavailability) |
|
|
151
|
+
| `defaultTimeSchedule` | `DateSchedule` | — | Fallback schedule for dates not present in `timeSchedule` |
|
|
152
|
+
|
|
153
|
+
#### Styling
|
|
154
|
+
|
|
155
|
+
| Prop | Type | Description |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `theme` | `DatePickerTheme` | CSS variable overrides (see [theming](#theming-via-css-custom-properties)) |
|
|
158
|
+
| `classNames` | `DatePickerClassNames` | Per-slot className overrides (see [slots](#per-slot-class--style-injection)) |
|
|
159
|
+
| `styles` | `DatePickerStyles` | Per-slot inline style overrides |
|
|
160
|
+
| `className` / `style` | `string` / `CSSProperties` | Extra class/style on the root element |
|
|
161
|
+
|
|
162
|
+
#### Custom renderers
|
|
163
|
+
|
|
164
|
+
| Prop | Signature | Description |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `renderDay` | `(date: Dayjs, props: TooltipRenderProps) => ReactNode` | Fully custom day cell content |
|
|
167
|
+
| `renderNavButton` | `(direction: 'prev' \| 'next', onClick: () => void) => ReactNode` | Custom prev/next month buttons |
|
|
168
|
+
| `renderInput` | `(props: RenderInputProps) => ReactNode` | Fully custom trigger input |
|
|
169
|
+
|
|
170
|
+
#### Tooltip
|
|
171
|
+
|
|
172
|
+
| Prop | Type | Default | Description |
|
|
173
|
+
|---|---|---|---|
|
|
174
|
+
| `tooltip` | `string \| ReactNode \| (props: TooltipRenderProps) => ReactNode` | — | Day tooltip content |
|
|
175
|
+
| `tooltipDelay` | `number` | `300` | Tooltip delay (ms) |
|
|
176
|
+
|
|
177
|
+
#### Events
|
|
178
|
+
|
|
179
|
+
| Prop | Signature | Description |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `onDateChange` | `(date: Dayjs \| null) => void` | Fires on single-date selection/clear |
|
|
182
|
+
| `onRangeChange` | `(range: { start, end }) => void` | Fires on range selection/clear |
|
|
183
|
+
| `onTimeChange` | `(date: Dayjs \| null, time: string) => void` | Fires when a time slot is picked |
|
|
184
|
+
| `onBeforeSelect` | `(date: Dayjs, meta: DateSelectionMeta) => boolean \| void` | Return `false` to cancel a selection |
|
|
185
|
+
| `onAfterSelect` | `(date: Dayjs, meta: DateSelectionMeta) => void` | Fires right after a selection is committed |
|
|
186
|
+
| `onMonthChange` | `(month: number, year: number) => void` | Fires when the visible month changes |
|
|
187
|
+
| `onYearChange` | `(year: number) => void` | Fires when the visible year changes |
|
|
188
|
+
| `onDayHover` | `(date: Dayjs \| null) => void` | Fires on day hover (range preview) |
|
|
189
|
+
| `onOpen` / `onClose` | `() => void` | Popover open/close |
|
|
190
|
+
| `onClear` | `() => void` | Fires when the clear button is used |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### `TimePicker` (standalone)
|
|
195
|
+
|
|
196
|
+
A self-contained time-of-day picker that can be used independently of `DatePicker` — as a popover-triggering input or rendered inline.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import { TimePicker } from '@daxence/flexy-date-picker';
|
|
200
|
+
|
|
201
|
+
<TimePicker
|
|
202
|
+
value={time}
|
|
203
|
+
onChange={setTime}
|
|
204
|
+
timeStep={15}
|
|
205
|
+
minTime="08:00"
|
|
206
|
+
maxTime="18:00"
|
|
207
|
+
/>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| Prop | Type | Default | Description |
|
|
211
|
+
|---|---|---|---|
|
|
212
|
+
| `value` | `string` (`HH:mm`) | — | Controlled value |
|
|
213
|
+
| `defaultValue` | `string` | — | Uncontrolled initial value |
|
|
214
|
+
| `onChange` | `(time: string) => void` | — | Called with the selected `HH:mm`, or `''` after clear |
|
|
215
|
+
| `timeStep` | `number` | `15` | Uniform slot interval in minutes (ignored when `schedule` is set) |
|
|
216
|
+
| `minTime` / `maxTime` | `string` (`HH:mm`) | `'00:00'` / `'23:59'` | Slot bounds (ignored when `schedule` is set) |
|
|
217
|
+
| `schedule` | `DateSchedule` | — | Schedule-based generation: multiple time ranges, each with its own step, with gaps between them producing no slots. Overrides `timeStep`/`minTime`/`maxTime` |
|
|
218
|
+
| `timeFormat` | `string` | `'HH:mm'` | dayjs format for slot labels and display |
|
|
219
|
+
| `placeholder` | `string` | — | Placeholder text when no value is selected |
|
|
220
|
+
| `locale` | `string` | — | dayjs locale |
|
|
221
|
+
| `date` | `Dayjs` | today | Reference date used by `disabledTime` |
|
|
222
|
+
| `disabledTime` | `DisabledTimeConfig` | — | Disable slots by weekday, date, or predicate |
|
|
223
|
+
| `inline` | `boolean` | `false` | Render the slot list directly, without an input/popover |
|
|
224
|
+
| `isDisabled` | `boolean` | `false` | Disable all interaction |
|
|
225
|
+
| `readOnly` | `boolean` | `false` | Show the value but prevent changes |
|
|
226
|
+
| `clearable` | `boolean` | `true` | Show the clear button |
|
|
227
|
+
| `onOpen` / `onClose` / `onClear` | `() => void` | — | Popover lifecycle events |
|
|
228
|
+
| `className` / `style` | `string` / `CSSProperties` | — | Root element overrides |
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### `CalendarLegend`
|
|
233
|
+
|
|
234
|
+
A small presentational component for labeling calendar states (e.g. availability legends in booking UIs).
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
import { CalendarLegend } from '@daxence/flexy-date-picker';
|
|
238
|
+
|
|
239
|
+
<CalendarLegend
|
|
240
|
+
items={[
|
|
241
|
+
{ label: 'Available', className: 'fldp-legend-box--available' },
|
|
242
|
+
{ label: 'Selected', className: 'fldp-legend-box--selected' },
|
|
243
|
+
{ label: 'Unavailable', className: 'fldp-legend-box--unavailable' },
|
|
244
|
+
]}
|
|
245
|
+
/>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
| Prop | Type | Default | Description |
|
|
249
|
+
|---|---|---|---|
|
|
250
|
+
| `items` | `{ label: string \| ReactNode; className: string }[]` | 5 default French labels | Legend rows, each rendered as a colored box + label |
|
|
251
|
+
| `className` | `string` | — | Extra class on the root element |
|
|
252
|
+
| `style` | `CSSProperties` | — | Extra inline style on the root element |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Theming via CSS custom properties
|
|
257
|
+
|
|
258
|
+
Override any token on a parent element:
|
|
259
|
+
|
|
260
|
+
```css
|
|
261
|
+
.my-app {
|
|
262
|
+
--fldp-primary: #7c3aed;
|
|
263
|
+
--fldp-primary-hover: #6d28d9;
|
|
264
|
+
--fldp-muted: #ede9fe;
|
|
265
|
+
--fldp-border-radius: 12px;
|
|
266
|
+
--fldp-day-border-radius: 50%;
|
|
267
|
+
--fldp-font-family: 'Inter', sans-serif;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Or pass the `theme` prop:
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
<DatePicker
|
|
275
|
+
lang="fr"
|
|
276
|
+
weekStartsOn={1}
|
|
277
|
+
theme={{
|
|
278
|
+
primary: '#7c3aed',
|
|
279
|
+
primaryHover: '#6d28d9',
|
|
280
|
+
muted: '#ede9fe',
|
|
281
|
+
borderRadius: '12px',
|
|
282
|
+
dayBorderRadius: '50%',
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### All available tokens
|
|
288
|
+
|
|
289
|
+
| Token | Default | Description |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| `--fldp-primary` | `#3b82f6` | Accent / selected color |
|
|
292
|
+
| `--fldp-primary-hover` | `#2563eb` | Accent hover |
|
|
293
|
+
| `--fldp-primary-foreground` | `#ffffff` | Text on accent background |
|
|
294
|
+
| `--fldp-background` | `#ffffff` | Popover background |
|
|
295
|
+
| `--fldp-surface` | `rgba(255, 255, 255, 0.88)` | Trigger + menu surface |
|
|
296
|
+
| `--fldp-surface-strong` | `rgba(255, 255, 255, 0.98)` | Strong surface tone |
|
|
297
|
+
| `--fldp-foreground` | `#0f172a` | Default text color |
|
|
298
|
+
| `--fldp-muted` | `#eff6ff` | In-range highlight |
|
|
299
|
+
| `--fldp-accent-soft` | `rgba(15, 118, 110, 0.12)` | Soft hover/accent state |
|
|
300
|
+
| `--fldp-muted-foreground` | `#64748b` | Placeholder / weekday text |
|
|
301
|
+
| `--fldp-border` | `#e2e8f0` | Border color |
|
|
302
|
+
| `--fldp-input-background` | `#ffffff` | Input background |
|
|
303
|
+
| `--fldp-today-color` | `#3b82f6` | Today dot + text |
|
|
304
|
+
| `--fldp-border-radius` | `8px` | Outer radius |
|
|
305
|
+
| `--fldp-day-border-radius` | `6px` | Day cell radius |
|
|
306
|
+
| `--fldp-shadow` | `…` | Popover box shadow |
|
|
307
|
+
| `--fldp-font-family` | system-ui | Font family |
|
|
308
|
+
| `--fldp-font-size` | `14px` | Base font size |
|
|
309
|
+
| `--fldp-disabled-opacity` | `0.38` | Disabled element opacity |
|
|
310
|
+
| `--fldp-transition-duration` | `150ms` | Animation speed |
|
|
311
|
+
| `--fldp-z-index` | `9999` | Popover z-index |
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Per-slot class / style injection
|
|
316
|
+
|
|
317
|
+
Every visual slot accepts a className and inline style:
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
<DatePicker
|
|
321
|
+
classNames={{
|
|
322
|
+
calendar: 'my-calendar',
|
|
323
|
+
daySelected: 'my-selected-day',
|
|
324
|
+
popover: 'my-popover',
|
|
325
|
+
}}
|
|
326
|
+
styles={{
|
|
327
|
+
popover: { background: '#fefce8' },
|
|
328
|
+
dayInRange: { fontWeight: 700 },
|
|
329
|
+
}}
|
|
330
|
+
/>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Available slots: `root`, `inputWrapper`, `input`, `separator`, `clearButton`, `popover`, `calendar`, `calendarHeader`, `navButton`, `monthYearLabel`, `weekdays`, `weekday`, `daysGrid`, `day`, `dayToday`, `daySelected`, `dayRangeStart`, `dayRangeEnd`, `dayInRange`, `dayDisabled`, `dayOutside`, `dayHovered`, `timePicker`, `timeTitle`, `timeSlotList`, `timeSlot`, `timeSlotSelected`, `timeSlotDisabled`, `tooltip`.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Tooltips
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
// Static string
|
|
341
|
+
<DatePicker tooltip="Click to select" />
|
|
342
|
+
|
|
343
|
+
// ReactNode
|
|
344
|
+
<DatePicker tooltip={<strong>Pick a date</strong>} />
|
|
345
|
+
|
|
346
|
+
// Render prop — full control, including hotel-style "X nights" hints
|
|
347
|
+
<DatePicker
|
|
348
|
+
mode="range"
|
|
349
|
+
tooltip={({ date, isToday, isDisabled, isRangeStart, isRangeEnd, isInRange, nights }) => {
|
|
350
|
+
if (isDisabled) return '🚫 Unavailable';
|
|
351
|
+
if (nights && isRangeEnd) return `${nights} night${nights > 1 ? 's' : ''}`;
|
|
352
|
+
if (isRangeStart) return '📌 Check-in';
|
|
353
|
+
if (isRangeEnd) return '🏁 Check-out';
|
|
354
|
+
if (isToday) return '📅 Today';
|
|
355
|
+
return date.format('dddd, MMM D');
|
|
356
|
+
}}
|
|
357
|
+
tooltipDelay={200}
|
|
358
|
+
/>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Disabled dates
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
<DatePicker
|
|
367
|
+
disabled={{
|
|
368
|
+
before: dayjs('2024-01-01'), // disable all before Jan 1
|
|
369
|
+
after: dayjs('2024-12-31'), // disable all after Dec 31
|
|
370
|
+
dates: [dayjs('2024-06-15')], // specific dates
|
|
371
|
+
predicate: (d) => d.day() === 0 || d.day() === 6, // weekends
|
|
372
|
+
}}
|
|
373
|
+
/>
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Date + time picker
|
|
379
|
+
|
|
380
|
+
Combine calendar selection with a time slot list in one component:
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
<DatePicker
|
|
384
|
+
mode="single"
|
|
385
|
+
value={meetingDate}
|
|
386
|
+
onDateChange={setMeetingDate}
|
|
387
|
+
onTimeChange={(date, time) => console.log(date, time)}
|
|
388
|
+
enableTime
|
|
389
|
+
timeStep={15}
|
|
390
|
+
timeFormat="HH:mm"
|
|
391
|
+
minTime="08:00"
|
|
392
|
+
maxTime="20:00"
|
|
393
|
+
disabledTime={{
|
|
394
|
+
weekdays: { 0: ['08:00', '08:15', '08:30'], 6: ['08:00', '08:15', '08:30'] },
|
|
395
|
+
}}
|
|
396
|
+
/>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Per-date time schedules
|
|
400
|
+
|
|
401
|
+
Give individual dates their own set of time ranges — with gaps, different step intervals, or full unavailability — via `timeSchedule` / `defaultTimeSchedule`:
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
<DatePicker
|
|
405
|
+
mode="single"
|
|
406
|
+
enableTime
|
|
407
|
+
defaultTimeSchedule={{ ranges: [{ from: '09:00', to: '17:00', step: 30 }] }}
|
|
408
|
+
timeSchedule={{
|
|
409
|
+
'2026-06-15': { unavailable: true }, // fully booked, no slots shown
|
|
410
|
+
'2026-06-16': {
|
|
411
|
+
ranges: [
|
|
412
|
+
{ from: '08:00', to: '12:00', step: 15 },
|
|
413
|
+
{ from: '14:00', to: '16:00', step: 5 },
|
|
414
|
+
{ from: '16:00', to: '18:00', step: 10 },
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
}}
|
|
418
|
+
/>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Range picker
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
const [range, setRange] = useState({ start: null, end: null });
|
|
427
|
+
|
|
428
|
+
<DatePicker
|
|
429
|
+
mode="range"
|
|
430
|
+
rangeValue={range}
|
|
431
|
+
onRangeChange={setRange}
|
|
432
|
+
numberOfMonths={2}
|
|
433
|
+
/>
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Custom renderers
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
<DatePicker
|
|
442
|
+
renderDay={(date, { isSelected }) => (
|
|
443
|
+
<span style={{ fontWeight: isSelected ? 700 : 400 }}>{date.date()}</span>
|
|
444
|
+
)}
|
|
445
|
+
renderNavButton={(direction, onClick) => (
|
|
446
|
+
<button onClick={onClick}>{direction === 'prev' ? '←' : '→'}</button>
|
|
447
|
+
)}
|
|
448
|
+
renderInput={({ value, onClick, isOpen }) => (
|
|
449
|
+
<button onClick={onClick}>{value || 'Pick a date'} {isOpen ? '▲' : '▼'}</button>
|
|
450
|
+
)}
|
|
451
|
+
/>
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Accessibility
|
|
457
|
+
|
|
458
|
+
The calendar grid, navigation, and inputs follow WAI-ARIA authoring practices out of the box:
|
|
459
|
+
|
|
460
|
+
- Day grid uses `role="grid"` / `role="row"` / `role="columnheader"`, with each day exposing `aria-label` (full localized date), `aria-pressed` (selected state), and `aria-disabled`.
|
|
461
|
+
- Month/year navigation exposes `aria-label="Previous month"` / `"Next month"`, and the month/year dropdowns use `role="listbox"` / `role="option"` with `aria-expanded` / `aria-selected`.
|
|
462
|
+
- The input trigger uses `role="button"`, `aria-haspopup="dialog"`, and `aria-expanded`; the popover itself uses `role="dialog"`.
|
|
463
|
+
- The standalone time picker region is exposed via `role="region"` with a descriptive `aria-label`.
|
|
464
|
+
- Decorative icons/carets are marked `aria-hidden="true"` so they're skipped by screen readers.
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Quality & testing
|
|
469
|
+
|
|
470
|
+
This project is built and gated the way a production library should be:
|
|
471
|
+
|
|
472
|
+
| Check | Status |
|
|
473
|
+
|---|---|
|
|
474
|
+
| **Unit test coverage** | 100% statements / branches / functions / lines, enforced as a hard CI gate (`vitest --coverage`, thresholds in [vite.config.ts](./vite.config.ts)) |
|
|
475
|
+
| **Test suite** | 100+ tests across every component, context, and utility (`vitest`, React Testing Library) |
|
|
476
|
+
| **Type safety** | `strict` TypeScript, zero `any` leaking through the public API, full `.d.ts` output |
|
|
477
|
+
| **Linting** | ESLint with `typescript-eslint`, `react-hooks`, `jsx-a11y`, and `unicorn` rule sets, zero warnings allowed |
|
|
478
|
+
| **Accessibility** | WAI-ARIA roles/attributes on every interactive element (see [Accessibility](#accessibility)) and an automated `@storybook/addon-a11y` check on every story |
|
|
479
|
+
| **Bundle** | Tree-shakeable ESM + UMD output via `vite-plugin-dts`, ~35 kB gzipped for the full library |
|
|
480
|
+
| **Commits & releases** | [Conventional Commits](https://www.conventionalcommits.org/) enforced via `commitlint` + `husky`, versioning and changelog automated with `semantic-release` |
|
|
481
|
+
|
|
482
|
+
Every prop, event, and edge case documented above (disabled dates, per-date time schedules, range reordering, keyboard navigation, placement auto-resolution, etc.) is backed by a dedicated test — the goal is that the README and the test suite never drift apart.
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
npm run test # Run the full suite
|
|
486
|
+
npm run test:coverage # Run with the 100% coverage gate enforced
|
|
487
|
+
npm run lint # Zero-warning lint pass
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Development
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
npm install
|
|
496
|
+
npm run dev # Vite dev server with the demo app (with copy-pasteable code snippets)
|
|
497
|
+
npm run build # Build the library to /dist
|
|
498
|
+
npm run test # Run the test suite
|
|
499
|
+
npm run test:coverage # Run tests with coverage
|
|
500
|
+
npm run storybook # Explore components interactively
|
|
501
|
+
npm run build-storybook # Build a static Storybook site
|
|
502
|
+
npm run lint # Lint the codebase
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Contributing
|
|
506
|
+
|
|
507
|
+
Issues and pull requests are welcome. Please:
|
|
508
|
+
|
|
509
|
+
1. Keep the codebase feature-based (`src/features/<feature>/...`) and add tests + Storybook stories for new behavior.
|
|
510
|
+
2. Run `npm run lint` and `npm run test` before opening a PR.
|
|
511
|
+
3. Follow [Conventional Commits](https://www.conventionalcommits.org/) — releases are automated with `semantic-release`.
|
|
512
|
+
|
|
513
|
+
## License
|
|
514
|
+
|
|
515
|
+
[MIT](./LICENSE) © [Daxence Solutions](https://github.com/daxence)
|