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