@carbon-labs/react-date-picker 0.2.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.
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Copyright IBM Corp. 2024
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { useRef, useState, useEffect, useCallback } from 'react';
9
+ import { DatePickerStateMachine, parseDateToPlainDate, DatePickerState, plainDateToDate, DatePickerEvent, ClickOutsideHandler, mapKeyboardToStateMachineEvent } from '@carbon-labs/primitives/date-picker';
10
+
11
+ /**
12
+ * Configuration for the useDatePicker hook
13
+ * Maintains 100% backwards compatibility with Carbon React v11 API
14
+ */
15
+
16
+ /**
17
+ * Return type for the useDatePicker hook
18
+ */
19
+
20
+ // Note: parseDate and temporalToDate functions removed - now using shared utilities
21
+ // from @carbon-labs/primitives/date-picker:
22
+ // - parseDateToPlainDate() handles mm/dd/yyyy, ISO, and Date object parsing
23
+ // - plainDateToDate() converts Temporal.PlainDate to Date objects
24
+
25
+ /**
26
+ * React hook for managing date picker state using the shared state machine
27
+ * Maintains 100% backwards compatibility with Carbon React v11 API
28
+ *
29
+ * @param {UseDatePickerConfig} config - Configuration options
30
+ * @returns {UseDatePickerReturn} Hook return object with state and handlers
31
+ */
32
+ function useDatePicker(config = {}) {
33
+ const {
34
+ datePickerType = 'single',
35
+ value = '',
36
+ minDate = null,
37
+ maxDate = null,
38
+ dateFormat = 'm/d/Y',
39
+ closeOnSelect = true,
40
+ disabled = false,
41
+ readOnly = false,
42
+ onChange,
43
+ onOpen,
44
+ onClose
45
+ } = config;
46
+
47
+ // Refs for input elements
48
+ const startInputRef = useRef(null);
49
+ const endInputRef = useRef(null);
50
+ const calendarRef = useRef(null);
51
+
52
+ // State machine instance (persists across renders)
53
+ const machineRef = useRef(null);
54
+
55
+ // React state for triggering re-renders
56
+ const [context, setContext] = useState(() => {
57
+ // Initialize state machine on first render
58
+ const machine = new DatePickerStateMachine({
59
+ mode: datePickerType,
60
+ value,
61
+ minDate: parseDateToPlainDate(minDate),
62
+ maxDate: parseDateToPlainDate(maxDate),
63
+ dateFormat,
64
+ isDisabled: disabled,
65
+ isReadonly: readOnly
66
+ });
67
+ machineRef.current = machine;
68
+ return machine.getContext();
69
+ });
70
+ const [state, setState] = useState(() => {
71
+ return machineRef.current?.getState() || DatePickerState.IDLE;
72
+ });
73
+
74
+ // Subscribe to state machine changes
75
+ useEffect(() => {
76
+ const machine = machineRef.current;
77
+ if (!machine) {
78
+ return;
79
+ }
80
+ const unsubscribe = machine.subscribe(transition => {
81
+ setContext(transition.context);
82
+ setState(transition.to);
83
+
84
+ // Call Carbon API callbacks
85
+ if (transition.to === DatePickerState.CALENDAR_OPEN && onOpen) {
86
+ onOpen();
87
+ }
88
+ if ((transition.to === DatePickerState.IDLE || transition.to === DatePickerState.FOCUSED) && transition.from === DatePickerState.CALENDAR_OPEN && onClose) {
89
+ onClose();
90
+ }
91
+ });
92
+ return unsubscribe;
93
+ }, [onOpen, onClose, context.isOpen]);
94
+
95
+ // Track previous dates to prevent infinite loops
96
+ const prevDatesRef = useRef('');
97
+
98
+ // Handle onChange callback (convert Temporal.PlainDate to Date[])
99
+ useEffect(() => {
100
+ if (!onChange) {
101
+ return;
102
+ }
103
+ const dates = [];
104
+ if (context.startDate) {
105
+ const date = plainDateToDate(context.startDate);
106
+ if (date) {
107
+ dates.push(date);
108
+ }
109
+ }
110
+ if (context.endDate) {
111
+ const date = plainDateToDate(context.endDate);
112
+ if (date) {
113
+ dates.push(date);
114
+ }
115
+ }
116
+
117
+ // Create a string representation of dates for comparison
118
+ const datesKey = dates.map(d => d.toISOString()).join(',');
119
+
120
+ // Only call onChange if dates have actually changed
121
+ if (dates.length > 0 && datesKey !== prevDatesRef.current) {
122
+ prevDatesRef.current = datesKey;
123
+ onChange(dates);
124
+ }
125
+ }, [context.startDate, context.endDate, onChange]);
126
+
127
+ // Update state machine when config changes
128
+ useEffect(() => {
129
+ const machine = machineRef.current;
130
+ if (!machine) {
131
+ return;
132
+ }
133
+
134
+ // Update disabled state
135
+ if (disabled !== context.isDisabled) {
136
+ machine.send(disabled ? DatePickerEvent.DISABLE : DatePickerEvent.ENABLE);
137
+ }
138
+
139
+ // Update readonly state
140
+ if (readOnly !== context.isReadonly) {
141
+ machine.send(readOnly ? DatePickerEvent.SET_READONLY : DatePickerEvent.UNSET_READONLY);
142
+ }
143
+
144
+ // Update min/max dates
145
+ const newMinDate = parseDateToPlainDate(minDate);
146
+ const newMaxDate = parseDateToPlainDate(maxDate);
147
+ if (newMinDate) {
148
+ machine.send(DatePickerEvent.SET_MIN_DATE, {
149
+ date: newMinDate
150
+ });
151
+ }
152
+ if (newMaxDate) {
153
+ machine.send(DatePickerEvent.SET_MAX_DATE, {
154
+ date: newMaxDate
155
+ });
156
+ }
157
+ }, [disabled, readOnly, minDate, maxDate, context.isDisabled, context.isReadonly]);
158
+
159
+ // Event handlers
160
+ const send = useCallback((eventType, payload) => {
161
+ machineRef.current?.send(eventType, payload);
162
+ }, []);
163
+ const openCalendar = useCallback(() => {
164
+ // Send CALENDAR_ICON_CLICK to trigger the state transition from IDLE
165
+ // The state machine will handle transitioning to CALENDAR_OPEN state
166
+ send(DatePickerEvent.CALENDAR_ICON_CLICK);
167
+ }, [send]);
168
+ const closeCalendar = useCallback(() => {
169
+ send(DatePickerEvent.CALENDAR_CLOSE);
170
+ }, [send]);
171
+ const selectDate = useCallback(date => {
172
+ if (datePickerType === 'range') {
173
+ if (!context.startDate || context.endDate) {
174
+ // Select start date
175
+ send(DatePickerEvent.RANGE_START_SELECT, {
176
+ date
177
+ });
178
+ } else {
179
+ // Select end date
180
+ send(DatePickerEvent.RANGE_END_SELECT, {
181
+ date
182
+ });
183
+ if (closeOnSelect) {
184
+ closeCalendar();
185
+ }
186
+ }
187
+ } else {
188
+ // Single date selection
189
+ send(DatePickerEvent.DATE_SELECT, {
190
+ date
191
+ });
192
+ if (closeOnSelect) {
193
+ closeCalendar();
194
+ }
195
+ }
196
+ }, [datePickerType, context.startDate, context.endDate, closeOnSelect, send, closeCalendar]);
197
+ const handleInputFocus = useCallback((inputType = 'from') => {
198
+ // Send INPUT_FOCUS to transition to FOCUSED state
199
+ send(DatePickerEvent.INPUT_FOCUS, {
200
+ inputType
201
+ });
202
+ // Then send CALENDAR_OPEN to open the calendar
203
+ // This matches the expected state machine flow: IDLE -> FOCUSED -> CALENDAR_OPEN
204
+ send(DatePickerEvent.CALENDAR_OPEN);
205
+ }, [send]);
206
+ const handleInputBlur = useCallback(() => {
207
+ send(DatePickerEvent.INPUT_BLUR);
208
+ }, [send]);
209
+ const handleInputChange = useCallback((value, inputType = 'from') => {
210
+ send(DatePickerEvent.VALUE_CHANGE, {
211
+ value,
212
+ inputType
213
+ });
214
+ }, [send]);
215
+
216
+ // Handle click outside to close calendar using shared utility
217
+ useEffect(() => {
218
+ if (!context.isOpen) {
219
+ return;
220
+ }
221
+ const handler = new ClickOutsideHandler({
222
+ isOpen: context.isOpen,
223
+ /**
224
+ * Check if a node is contained within the date picker elements
225
+ * @param {Node} node - The node to check
226
+ * @returns {boolean} True if the node is within the date picker
227
+ */
228
+ containsNode: node => {
229
+ const calendarEl = calendarRef.current;
230
+ const startInputEl = startInputRef.current;
231
+ const endInputEl = endInputRef.current;
232
+ return (calendarEl?.contains(node) ?? false) || (startInputEl?.contains(node) ?? false) || (endInputEl?.contains(node) ?? false);
233
+ },
234
+ /**
235
+ * Handle clicks outside the date picker
236
+ */
237
+ onOutsideClick: () => send(DatePickerEvent.OUTSIDE_CLICK),
238
+ useCapture: true,
239
+ attachDelay: 0
240
+ });
241
+ handler.attach();
242
+ return () => {
243
+ handler.detach();
244
+ };
245
+ }, [context.isOpen, send]);
246
+
247
+ // Handle keyboard events
248
+ useEffect(() => {
249
+ if (!context.isOpen) {
250
+ return;
251
+ }
252
+
253
+ /**
254
+ * Handle keyboard events for calendar navigation
255
+ *
256
+ * @param {KeyboardEvent} event - Keyboard event
257
+ */
258
+ const handleKeyDown = event => {
259
+ const {
260
+ key
261
+ } = event;
262
+ const target = event.target;
263
+
264
+ // Check if focus is in the calendar (not in input fields)
265
+ const calendarEl = calendarRef.current;
266
+ const startInputEl = startInputRef.current;
267
+ const endInputEl = endInputRef.current;
268
+ const isFocusInCalendar = calendarEl && (calendarEl.contains(target) || target === calendarEl || target.classList?.contains('cds--date-picker__calendar'));
269
+ const isFocusInInput = target === startInputEl || target === endInputEl || startInputEl && startInputEl.contains(target) || endInputEl && endInputEl.contains(target);
270
+
271
+ // Handle Escape key - close calendar (works from anywhere)
272
+ if (key === 'Escape') {
273
+ event.preventDefault();
274
+ send(DatePickerEvent.ESCAPE_KEY);
275
+ return;
276
+ }
277
+
278
+ // Handle Tab key - complex focus management
279
+ if (key === 'Tab') {
280
+ // Case 1: Tab FROM input -> Focus the calendar container
281
+ if (isFocusInInput && !event.shiftKey) {
282
+ event.preventDefault();
283
+ // Focus the calendar container which has tabIndex={0}
284
+ if (calendarEl) {
285
+ setTimeout(() => {
286
+ const calendar = calendarEl.querySelector('.cds--date-picker__calendar');
287
+ if (calendar) {
288
+ calendar.focus();
289
+ }
290
+ }, 0);
291
+ }
292
+ return;
293
+ }
294
+
295
+ // Case 2: Shift+Tab FROM calendar -> Focus input
296
+ if (isFocusInCalendar && event.shiftKey) {
297
+ event.preventDefault();
298
+ // Focus the appropriate input based on mode
299
+ if (datePickerType === 'range' && context.lastFocusedInput === 'to' && endInputEl) {
300
+ endInputEl.focus();
301
+ } else if (startInputEl) {
302
+ startInputEl.focus();
303
+ }
304
+ return;
305
+ }
306
+
307
+ // Case 3: Tab FROM calendar -> Close and move to next element
308
+ if (isFocusInCalendar && !event.shiftKey) {
309
+ // Let the state machine handle closing
310
+ send(DatePickerEvent.TAB_KEY);
311
+ // Don't prevent default - let browser move focus naturally
312
+ return;
313
+ }
314
+
315
+ // For other cases, let default Tab behavior work
316
+ return;
317
+ }
318
+
319
+ // Only handle navigation keys when focus is in calendar, not in input
320
+ if (!isFocusInCalendar || isFocusInInput) {
321
+ return;
322
+ }
323
+
324
+ // Use shared keyboard mapper for navigation keys
325
+ const mappedEvent = mapKeyboardToStateMachineEvent({
326
+ key,
327
+ shiftKey: event.shiftKey,
328
+ mode: datePickerType,
329
+ state,
330
+ focusedDate: context.focusedDate
331
+ });
332
+ if (mappedEvent) {
333
+ if (mappedEvent.preventDefault) {
334
+ event.preventDefault();
335
+ }
336
+ if (mappedEvent.eventType) {
337
+ send(mappedEvent.eventType, mappedEvent.payload);
338
+ }
339
+ return;
340
+ }
341
+ };
342
+ document.addEventListener('keydown', handleKeyDown);
343
+ return () => {
344
+ document.removeEventListener('keydown', handleKeyDown);
345
+ };
346
+ }, [context.isOpen, context.focusedDate, context.lastFocusedInput, state, datePickerType, send]);
347
+ return {
348
+ context,
349
+ state,
350
+ isOpen: context.isOpen,
351
+ send,
352
+ openCalendar,
353
+ closeCalendar,
354
+ selectDate,
355
+ handleInputFocus,
356
+ handleInputBlur,
357
+ handleInputChange,
358
+ startInputRef,
359
+ endInputRef,
360
+ calendarRef
361
+ };
362
+ }
363
+
364
+ export { useDatePicker };
package/es/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ export { DatePicker } from './components/DatePicker';
8
+ export { DatePickerInput } from './components/DatePickerInput';
9
+ export { Calendar } from './components/Calendar';
10
+ export { DatePickerSkeleton } from './components/DatePickerSkeleton';
11
+ export { useDatePicker } from './hooks/useDatePicker';
12
+ export type { DatePickerProps } from './components/DatePicker';
13
+ export type { DatePickerInputProps } from './components/DatePickerInput';
14
+ export type { CalendarProps } from './components/Calendar';
15
+ export type { DatePickerSkeletonProps } from './components/DatePickerSkeleton';
16
+ export type { UseDatePickerConfig, UseDatePickerReturn, } from './hooks/useDatePicker';
17
+ export type { DatePickerContext, DatePickerMode, InputType, DateSelectPayload, InputFocusPayload, KeyboardPayload, ValueChangePayload, ValidationErrorPayload, StateTransition, } from '@carbon-labs/primitives/date-picker';
18
+ export { DatePickerState, DatePickerEvent, } from '@carbon-labs/primitives/date-picker';
package/es/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Copyright IBM Corp. 2024
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ export { DatePicker } from './components/DatePicker.js';
9
+ export { DatePickerInput } from './components/DatePickerInput.js';
10
+ export { Calendar } from './components/Calendar.js';
11
+ export { DatePickerSkeleton } from './components/DatePickerSkeleton.js';
12
+ export { useDatePicker } from './hooks/useDatePicker.js';
13
+ export { DatePickerEvent, DatePickerState } from '@carbon-labs/primitives/date-picker';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Copyright IBM Corp. 2024
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ function _extends() {
11
+ return _extends = Object.assign ? Object.assign.bind() : function (n) {
12
+ for (var e = 1; e < arguments.length; e++) {
13
+ var t = arguments[e];
14
+ for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
15
+ }
16
+ return n;
17
+ }, _extends.apply(null, arguments);
18
+ }
19
+
20
+ exports.extends = _extends;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import React from 'react';
8
+ import type { DatePickerContext } from '@carbon-labs/primitives/date-picker';
9
+ /**
10
+ * Calendar component props
11
+ */
12
+ export interface CalendarProps {
13
+ /**
14
+ * State machine context
15
+ */
16
+ context: DatePickerContext;
17
+ /**
18
+ * Date selection handler
19
+ */
20
+ onDateSelect: (date: Temporal.PlainDate) => void;
21
+ /**
22
+ * Navigation handler
23
+ */
24
+ onNavigate: (eventType: string) => void;
25
+ /**
26
+ * Additional CSS class names
27
+ */
28
+ className?: string;
29
+ }
30
+ /**
31
+ * Calendar component
32
+ * Renders a calendar grid for date selection
33
+ */
34
+ export declare function Calendar({ context, onDateSelect, onNavigate, className, }: CalendarProps): React.JSX.Element | null;
35
+ export declare namespace Calendar {
36
+ var displayName: string;
37
+ }