@drivenets/design-system 0.7.3 → 0.8.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/index.cjs +4445 -3769
  3. package/dist/index.d.cts +1224 -945
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1224 -945
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4446 -3776
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +7 -8
  11. package/src/components/ds-autocomplete/ds-autocomplete.module.scss +1 -1
  12. package/src/components/ds-autocomplete/ds-autocomplete.tsx +13 -8
  13. package/src/components/ds-autocomplete/ds-autocomplete.types.ts +2 -1
  14. package/src/components/ds-comment-bubble/ds-comment-bubble.module.scss +6 -2
  15. package/src/components/ds-comment-bubble/ds-comment-bubble.tsx +22 -15
  16. package/src/components/ds-comment-bubble/ds-comment-bubble.types.ts +1 -0
  17. package/src/components/ds-date-input/ds-date-input.tsx +3 -4
  18. package/src/components/ds-date-input/ds-date-input.types.ts +3 -1
  19. package/src/components/ds-date-input/index.ts +10 -0
  20. package/src/components/ds-date-picker/components/calendar-table.module.scss +93 -0
  21. package/src/components/ds-date-picker/components/calendar-view-control/calendar-view-control.module.scss +48 -0
  22. package/src/components/ds-date-picker/components/calendar-view-control/calendar-view-control.tsx +20 -0
  23. package/src/components/ds-date-picker/components/calendar-view-control/index.ts +1 -0
  24. package/src/components/ds-date-picker/components/day-view/day-view.module.scss +12 -0
  25. package/src/components/ds-date-picker/components/day-view/day-view.tsx +45 -0
  26. package/src/components/ds-date-picker/components/day-view/index.ts +1 -0
  27. package/src/components/ds-date-picker/components/month-view/index.ts +1 -0
  28. package/src/components/ds-date-picker/components/month-view/month-view.module.scss +4 -0
  29. package/src/components/ds-date-picker/components/month-view/month-view.tsx +35 -0
  30. package/src/components/ds-date-picker/components/year-view/index.ts +1 -0
  31. package/src/components/ds-date-picker/components/year-view/year-view.module.scss +4 -0
  32. package/src/components/ds-date-picker/components/year-view/year-view.tsx +35 -0
  33. package/src/components/ds-date-picker/ds-date-picker.module.scss +19 -0
  34. package/src/components/ds-date-picker/ds-date-picker.tsx +209 -0
  35. package/src/components/ds-date-picker/ds-date-picker.types.ts +94 -0
  36. package/src/components/ds-date-picker/ds-date-picker.utils.ts +87 -0
  37. package/src/components/ds-date-picker/ds-date-picker.utils.unit.test.ts +226 -0
  38. package/src/components/ds-date-picker/index.ts +2 -0
  39. package/src/components/ds-date-range-picker/ds-date-range-picker.module.scss +20 -0
  40. package/src/components/ds-date-range-picker/ds-date-range-picker.tsx +101 -0
  41. package/src/components/ds-date-range-picker/ds-date-range-picker.types.ts +95 -0
  42. package/src/components/ds-date-range-picker/ds-date-range-picker.utils.ts +19 -0
  43. package/src/components/ds-date-range-picker/ds-date-range-picker.utils.unit.test.ts +48 -0
  44. package/src/components/ds-date-range-picker/index.ts +9 -0
  45. package/src/components/ds-file-upload/ds-file-upload-api.types.ts +1 -0
  46. package/src/components/ds-file-upload/hooks/use-file-upload.ts +1 -1
  47. package/src/components/ds-file-upload/index.ts +6 -1
  48. package/src/components/ds-file-upload/utils/file-factory.ts +1 -0
  49. package/src/components/ds-file-upload/utils/file-factory.unit.test.ts +37 -0
  50. package/src/components/ds-form-control/ds-form-control.tsx +5 -0
  51. package/src/components/ds-key-value-pair/ds-key-value-pair.module.scss +72 -0
  52. package/src/components/ds-key-value-pair/ds-key-value-pair.tsx +47 -0
  53. package/src/components/ds-key-value-pair/ds-key-value-pair.types.ts +24 -0
  54. package/src/components/ds-key-value-pair/index.ts +3 -0
  55. package/src/components/ds-loader/ds-loader.tsx +1 -1
  56. package/src/components/ds-status-badge/ds-status-badge.module.scss +1 -1
  57. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +4 -2
  58. package/src/components/ds-time-picker/components/time-item/index.ts +1 -0
  59. package/src/components/ds-time-picker/components/time-item/time-item.module.scss +37 -0
  60. package/src/components/ds-time-picker/components/time-item/time-item.tsx +26 -0
  61. package/src/components/ds-time-picker/components/time-item/time-item.types.ts +10 -0
  62. package/src/components/ds-time-picker/components/time-scroller/index.ts +2 -0
  63. package/src/components/ds-time-picker/components/time-scroller/time-scroller.module.scss +25 -0
  64. package/src/components/ds-time-picker/components/time-scroller/time-scroller.tsx +61 -0
  65. package/src/components/ds-time-picker/components/time-scroller/time-scroller.types.ts +17 -0
  66. package/src/components/ds-time-picker/components/time-scroller/time-scroller.utils.ts +44 -0
  67. package/src/components/ds-time-picker/ds-time-picker.tsx +158 -0
  68. package/src/components/ds-time-picker/ds-time-picker.types.ts +48 -0
  69. package/src/components/ds-time-picker/ds-time-picker.utils.ts +176 -0
  70. package/src/components/ds-time-picker/ds-time-picker.utils.unit.test.ts +316 -0
  71. package/src/components/ds-time-picker/index.ts +3 -0
  72. package/src/components/ds-tooltip/ds-tooltip.module.scss +1 -7
  73. package/src/components/ds-tooltip/ds-tooltip.tsx +9 -9
  74. package/src/components/ds-tooltip/ds-tooltip.types.ts +7 -1
  75. package/src/index.ts +4 -0
  76. package/src/styles/_root_updated.scss +593 -0
  77. package/src/styles/shared/_dropdown.scss +4 -1
  78. package/src/utils/use-controlled.browser.test.ts +79 -0
  79. package/src/utils/use-controlled.ts +18 -0
@@ -0,0 +1,209 @@
1
+ import { type ChangeEvent, Fragment, useEffect, useRef, useState } from 'react';
2
+ import { DatePicker, type DateValue } from '@ark-ui/react/date-picker';
3
+ import { Portal } from '@ark-ui/react/portal';
4
+ import type { DsDatePickerProps } from './ds-date-picker.types';
5
+ import {
6
+ formatDate,
7
+ fromIntlDate,
8
+ isSameDay,
9
+ parseDate,
10
+ toIntlDate,
11
+ validateDateString,
12
+ } from './ds-date-picker.utils';
13
+ import { DsTimePicker } from '../ds-time-picker';
14
+ import { DsIcon } from '../ds-icon';
15
+ import styles from './ds-date-picker.module.scss';
16
+ import { useControlled } from '../../utils/use-controlled';
17
+ import { DsTextInput } from '../ds-text-input';
18
+ import { DsButton } from '../ds-button';
19
+ import { DayView } from './components/day-view';
20
+ import { MonthView } from './components/month-view';
21
+ import { YearView } from './components/year-view';
22
+
23
+ const DsDatePicker = ({
24
+ placeholder,
25
+ className,
26
+ withTime = false,
27
+ closeOnSelect = true,
28
+ disablePortal = false,
29
+ hideClearButton = false,
30
+ locale,
31
+ slotProps,
32
+ onChange,
33
+ onOpenChange,
34
+ value: _value,
35
+ defaultValue: _defaultValue,
36
+ min: _min,
37
+ max: _max,
38
+ disabled,
39
+ readOnly,
40
+ id,
41
+ ...restProps
42
+ }: DsDatePickerProps) => {
43
+ const defaultValue = toIntlDate(_defaultValue);
44
+
45
+ const [value, setValue] = useControlled<DateValue | null>(
46
+ toIntlDate(_value),
47
+ onChange ? (v) => onChange(fromIntlDate(v)) : undefined,
48
+ defaultValue ?? null,
49
+ );
50
+
51
+ const min = toIntlDate(_min);
52
+ const max = toIntlDate(_max);
53
+
54
+ const [isFocused, setIsFocused] = useState(false);
55
+ const [isOpen, setIsOpen] = useState(false);
56
+
57
+ const inputRef = useRef<HTMLInputElement>(null);
58
+
59
+ const formattedValue = formatDate(value, withTime);
60
+
61
+ // Reformat on value change, but skip while focused to avoid disrupting typing
62
+ useEffect(() => {
63
+ if (!isFocused && inputRef.current) {
64
+ inputRef.current.value = formattedValue;
65
+ }
66
+ }, [isFocused, formattedValue]);
67
+
68
+ const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
69
+ const text = e.target.value;
70
+
71
+ const isValid = validateDateString({ text, min, max, withTime });
72
+
73
+ if (isValid) {
74
+ const parsedValue = parseDate(text);
75
+ setValue(parsedValue);
76
+ }
77
+ };
78
+
79
+ const resetInput = () => {
80
+ if (inputRef.current) {
81
+ inputRef.current.value = value ? formattedValue : '';
82
+ }
83
+ };
84
+
85
+ const handleFocus = () => {
86
+ setIsFocused(true);
87
+ };
88
+
89
+ const handleBlur = () => {
90
+ setIsFocused(false);
91
+
92
+ const text = inputRef.current?.value || '';
93
+ const isValid = validateDateString({ text, min, max, withTime });
94
+
95
+ if (isValid) {
96
+ setValue(parseDate(text));
97
+ } else {
98
+ resetInput();
99
+ }
100
+ };
101
+
102
+ const showClearButton = !hideClearButton && !disabled && !readOnly && !!value;
103
+ const Wrapper = disablePortal ? Fragment : Portal;
104
+
105
+ return (
106
+ <DatePicker.Root
107
+ className={className}
108
+ value={value ? [value] : undefined}
109
+ defaultValue={defaultValue ? [defaultValue] : undefined}
110
+ onValueChange={(details) => {
111
+ setValue(details.value[0] ?? null);
112
+ }}
113
+ selectionMode="single"
114
+ positioning={{ placement: 'bottom-start', gutter: 4 }}
115
+ closeOnSelect={withTime ? false : closeOnSelect}
116
+ min={min}
117
+ max={max}
118
+ disabled={disabled}
119
+ readOnly={readOnly}
120
+ open={isOpen}
121
+ onOpenChange={(details) => {
122
+ if (!isFocused) {
123
+ setIsOpen(details.open);
124
+ onOpenChange?.(details.open);
125
+ }
126
+ }}
127
+ {...restProps}
128
+ >
129
+ <DatePicker.Control className={styles.control}>
130
+ <DsTextInput
131
+ ref={inputRef}
132
+ id={id}
133
+ placeholder={placeholder ?? (withTime ? 'mm/dd/yyyy, hh:mm AM/PM' : 'mm/dd/yyyy')}
134
+ defaultValue={formattedValue}
135
+ onChange={handleInputChange}
136
+ onFocus={handleFocus}
137
+ onBlur={handleBlur}
138
+ disabled={disabled}
139
+ readOnly={readOnly}
140
+ {...slotProps?.input}
141
+ slots={{
142
+ ...slotProps?.input?.slots,
143
+ endAdornment: (
144
+ <>
145
+ {showClearButton && (
146
+ <DatePicker.ClearTrigger asChild onClick={() => setValue(null)}>
147
+ <DsButton
148
+ design="v1.2"
149
+ size="tiny"
150
+ buttonType="tertiary"
151
+ disabled={disabled}
152
+ aria-label={locale?.clearLabel ?? 'Clear date'}
153
+ >
154
+ <DsIcon icon="close" size="tiny" />
155
+ </DsButton>
156
+ </DatePicker.ClearTrigger>
157
+ )}
158
+ <DatePicker.Trigger asChild>
159
+ <DsButton
160
+ design="v1.2"
161
+ size="tiny"
162
+ buttonType="tertiary"
163
+ disabled={disabled}
164
+ aria-label={locale?.openCalendarLabel ?? 'Open calendar'}
165
+ >
166
+ <DsIcon icon="calendar_today" size="tiny" />
167
+ </DsButton>
168
+ </DatePicker.Trigger>
169
+ </>
170
+ ),
171
+ }}
172
+ />
173
+ </DatePicker.Control>
174
+
175
+ <Wrapper>
176
+ <DatePicker.Positioner className={styles.positioner}>
177
+ <DatePicker.Content className={styles.content}>
178
+ <DayView>
179
+ {withTime && (
180
+ <DsTimePicker
181
+ value={fromIntlDate(value)}
182
+ onChange={(v) => {
183
+ setValue(toIntlDate(v));
184
+ }}
185
+ min={isSameDay(value, min) ? _min : undefined}
186
+ max={isSameDay(value, max) ? _max : undefined}
187
+ disabled={disabled || !value}
188
+ readOnly={readOnly}
189
+ {...slotProps?.timePicker}
190
+ slotProps={{
191
+ popover: {
192
+ ...slotProps?.timePicker?.slotProps?.popover,
193
+ className: styles.timePickerPopoverContent,
194
+ },
195
+ ...slotProps?.timePicker?.slotProps,
196
+ }}
197
+ />
198
+ )}
199
+ </DayView>
200
+ <MonthView />
201
+ <YearView />
202
+ </DatePicker.Content>
203
+ </DatePicker.Positioner>
204
+ </Wrapper>
205
+ </DatePicker.Root>
206
+ );
207
+ };
208
+
209
+ export default DsDatePicker;
@@ -0,0 +1,94 @@
1
+ import type { DatePickerRootProps } from '@ark-ui/react/date-picker';
2
+ import type { DsTextInputProps } from '../ds-text-input';
3
+ import type { DsTimePickerProps } from '../ds-time-picker';
4
+
5
+ export interface DsDatePickerLocale {
6
+ openCalendarLabel?: string;
7
+ timeTriggerLabel?: string;
8
+ clearLabel?: string;
9
+ }
10
+
11
+ export interface DsDatePickerSlotProps {
12
+ input?: DsTextInputProps;
13
+ timePicker?: DsTimePickerProps;
14
+ }
15
+
16
+ export interface DsDatePickerProps extends Pick<
17
+ DatePickerRootProps,
18
+ 'disabled' | 'readOnly' | 'id' | 'open' | 'name'
19
+ > {
20
+ /**
21
+ * Controlled value as Date object.
22
+ * `null` means the date is empty.
23
+ * `undefined` means value is not provided, so component will be uncontrolled.
24
+ */
25
+ value?: Date | null;
26
+
27
+ /**
28
+ * Default value for uncontrolled mode
29
+ */
30
+ defaultValue?: Date;
31
+
32
+ /**
33
+ * Callback when the value changes
34
+ * @param value - Date object or null when cleared
35
+ */
36
+ onChange?: (value: Date | null) => void;
37
+
38
+ /**
39
+ * Callback when the calendar open state changes
40
+ */
41
+ onOpenChange?: (open: boolean) => void;
42
+
43
+ /**
44
+ * Minimum allowed datetime as Date object
45
+ */
46
+ min?: Date;
47
+
48
+ /**
49
+ * Maximum allowed datetime as Date object
50
+ */
51
+ max?: Date;
52
+
53
+ /**
54
+ * Whether to show the time picker alongside the date picker
55
+ * @default false
56
+ */
57
+ withTime?: boolean;
58
+
59
+ /**
60
+ * Placeholder text for the input
61
+ * @default 'mm/dd/yyyy'
62
+ */
63
+ placeholder?: string;
64
+
65
+ /**
66
+ * Whether to close the calendar when a date is selected. This is ignored when withTime is true.
67
+ * @default true
68
+ */
69
+ closeOnSelect?: boolean;
70
+
71
+ className?: string;
72
+
73
+ /**
74
+ * Whether to disable the portal for the calendar content
75
+ * @default false
76
+ */
77
+ disablePortal?: boolean;
78
+
79
+ /**
80
+ * Whether to hide the clear button
81
+ * @default true
82
+ */
83
+ hideClearButton?: boolean;
84
+
85
+ /**
86
+ * Localizable aria-labels and text
87
+ */
88
+ locale?: DsDatePickerLocale;
89
+
90
+ /**
91
+ * Props forwarded to sub-component slots
92
+ */
93
+ slotProps?: DsDatePickerSlotProps;
94
+ }
@@ -0,0 +1,87 @@
1
+ import { type DateValue } from '@ark-ui/react/date-picker';
2
+ import { fromDate, getLocalTimeZone } from '@internationalized/date';
3
+
4
+ export const toIntlDate = <T extends Date | null | undefined>(date: T): T extends Date ? DateValue : T =>
5
+ date ? (fromDate(date, getLocalTimeZone()) as never) : (date as never);
6
+
7
+ export const fromIntlDate = <T extends DateValue | null | undefined>(
8
+ date: T,
9
+ ): T extends DateValue ? Date : T => (date ? (date.toDate(getLocalTimeZone()) as never) : (date as never));
10
+
11
+ /**
12
+ * Format a DateValue to string (MM/DD/YYYY or MM/DD/YYYY hh:mm AM/PM)
13
+ */
14
+ export const formatDate = (date: DateValue | null, withTime = false): string => {
15
+ if (!date) {
16
+ return '';
17
+ }
18
+
19
+ const options: Intl.DateTimeFormatOptions = {
20
+ year: 'numeric',
21
+ month: '2-digit',
22
+ day: '2-digit',
23
+ };
24
+
25
+ if (withTime) {
26
+ options.hour = '2-digit';
27
+ options.minute = '2-digit';
28
+ options.hour12 = true;
29
+ }
30
+
31
+ return new Intl.DateTimeFormat('en-US', options).format(fromIntlDate(date));
32
+ };
33
+
34
+ /**
35
+ * Parse a date string (MM/DD/YYYY or MM/DD/YYYY, hh:mm AM/PM) to DateValue
36
+ */
37
+ export const parseDate = (dateStr: string): DateValue | null => {
38
+ const parsed = new Date(dateStr);
39
+ if (isNaN(parsed.getTime())) {
40
+ return null;
41
+ }
42
+
43
+ return fromDate(parsed, getLocalTimeZone());
44
+ };
45
+
46
+ /**
47
+ * Validate date string
48
+ */
49
+ export const validateDateString = ({
50
+ text,
51
+ min,
52
+ max,
53
+ }: {
54
+ text: string;
55
+ min?: DateValue | null;
56
+ max?: DateValue | null;
57
+ withTime?: boolean;
58
+ }): boolean => {
59
+ if (!text.trim()) {
60
+ return true;
61
+ }
62
+
63
+ const date = parseDate(text);
64
+ if (!date) {
65
+ return false;
66
+ }
67
+
68
+ if (min && date.compare(min) < 0) {
69
+ return false;
70
+ }
71
+ if (max && date.compare(max) > 0) {
72
+ return false;
73
+ }
74
+
75
+ return true;
76
+ };
77
+
78
+ export const isSameDay = (
79
+ date1: DateValue | null | undefined,
80
+ date2: DateValue | null | undefined,
81
+ ): boolean => {
82
+ if (!date1 || !date2) {
83
+ return false;
84
+ }
85
+
86
+ return date1.year === date2.year && date1.month === date2.month && date1.day === date2.day;
87
+ };
@@ -0,0 +1,226 @@
1
+ import { CalendarDate, CalendarDateTime, ZonedDateTime } from '@internationalized/date';
2
+ import { assert, describe, expect, it } from 'vitest';
3
+ import {
4
+ formatDate,
5
+ fromIntlDate,
6
+ isSameDay,
7
+ parseDate,
8
+ toIntlDate,
9
+ validateDateString,
10
+ } from './ds-date-picker.utils';
11
+
12
+ const createDate = (year: number, month: number, day: number, hours = 0, minutes = 0) => {
13
+ const d = new Date(year, month - 1, day);
14
+ d.setHours(hours, minutes, 0, 0);
15
+ return d;
16
+ };
17
+
18
+ const createDateValue = (year: number, month: number, day: number, hours?: number, minutes?: number) => {
19
+ if (hours !== undefined && minutes !== undefined) {
20
+ return new CalendarDateTime(year, month, day, hours, minutes, 0);
21
+ }
22
+ return new CalendarDate(year, month, day);
23
+ };
24
+
25
+ function assertDateValue(value: unknown): asserts value is CalendarDate | CalendarDateTime {
26
+ expect(value).toBeDefined();
27
+ expect(value).toHaveProperty('year');
28
+ expect(value).toHaveProperty('month');
29
+ expect(value).toHaveProperty('day');
30
+ }
31
+
32
+ describe('Date Picker Utils', () => {
33
+ describe('toIntlDate', () => {
34
+ it('should return null for null input', () => {
35
+ expect(toIntlDate(null)).toBeNull();
36
+ });
37
+
38
+ it('should return undefined for undefined input', () => {
39
+ // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
40
+ expect(toIntlDate(undefined)).toBeUndefined();
41
+ });
42
+
43
+ it('should convert valid Date to DateValue with correct year/month/day', () => {
44
+ const date = createDate(2024, 3, 15);
45
+ const result = toIntlDate(date);
46
+
47
+ assertDateValue(result);
48
+ expect(result.year).toBe(2024);
49
+ expect(result.month).toBe(3);
50
+ expect(result.day).toBe(15);
51
+ });
52
+ });
53
+
54
+ describe('fromIntlDate', () => {
55
+ it('should return null for null input', () => {
56
+ expect(fromIntlDate(null)).toBeNull();
57
+ });
58
+
59
+ it('should return undefined for undefined input', () => {
60
+ // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
61
+ expect(fromIntlDate(undefined)).toBeUndefined();
62
+ });
63
+
64
+ it('should convert valid DateValue to Date with correct year/month/day', () => {
65
+ const dateValue = createDateValue(2024, 3, 15);
66
+ const result = fromIntlDate(dateValue);
67
+
68
+ expect(result).toBeInstanceOf(Date);
69
+ expect(result.getFullYear()).toBe(2024);
70
+ expect(result.getMonth()).toBe(2);
71
+ expect(result.getDate()).toBe(15);
72
+ });
73
+
74
+ it('should handle CalendarDateTime with time components', () => {
75
+ const dateValue = createDateValue(2024, 3, 15, 14, 30);
76
+ const result = fromIntlDate(dateValue);
77
+
78
+ expect(result).toBeInstanceOf(Date);
79
+ expect(result.getHours()).toBe(14);
80
+ expect(result.getMinutes()).toBe(30);
81
+ });
82
+ });
83
+
84
+ describe('formatDate', () => {
85
+ it('should return empty string for null', () => {
86
+ expect(formatDate(null)).toBe('');
87
+ });
88
+
89
+ it('should format date as MM/DD/YYYY with zero-padding', () => {
90
+ const dateValue = createDateValue(2024, 1, 5);
91
+ expect(formatDate(dateValue)).toBe('01/05/2024');
92
+ });
93
+
94
+ it('should format date with time when withTime=true', () => {
95
+ const dateValue = createDateValue(2024, 3, 15, 14, 30);
96
+ expect(formatDate(dateValue, true)).toBe('03/15/2024, 02:30 PM');
97
+ });
98
+
99
+ it('should format midnight and noon correctly', () => {
100
+ expect(formatDate(createDateValue(2024, 3, 15, 0, 0), true)).toBe('03/15/2024, 12:00 AM');
101
+ expect(formatDate(createDateValue(2024, 3, 15, 12, 0), true)).toBe('03/15/2024, 12:00 PM');
102
+ });
103
+ });
104
+
105
+ describe('parseDate', () => {
106
+ it('should return null for empty or whitespace string', () => {
107
+ expect(parseDate('')).toBeNull();
108
+ expect(parseDate(' ')).toBeNull();
109
+ });
110
+
111
+ it('should parse valid MM/DD/YYYY format', () => {
112
+ const result = parseDate('03/15/2024');
113
+
114
+ assertDateValue(result);
115
+ expect(result.year).toBe(2024);
116
+ expect(result.month).toBe(3);
117
+ expect(result.day).toBe(15);
118
+ });
119
+
120
+ it('should parse valid MM/DD/YYYY hh:mm AM/PM format when withTime=true', () => {
121
+ const result = parseDate('03/15/2024 02:30 PM');
122
+
123
+ assertDateValue(result);
124
+ assert(result instanceof ZonedDateTime);
125
+ expect(result.hour).toBe(14);
126
+ expect(result.minute).toBe(30);
127
+ });
128
+
129
+ it('should return null for invalid format', () => {
130
+ expect(parseDate('not a date')).toBeNull();
131
+ });
132
+
133
+ it('should convert 12 AM to hour 0 and keep 12 PM as hour 12', () => {
134
+ const midnight = parseDate('03/15/2024 12:00 AM');
135
+ const noon = parseDate('03/15/2024 12:00 PM');
136
+
137
+ assert(midnight instanceof ZonedDateTime);
138
+ assert(noon instanceof ZonedDateTime);
139
+
140
+ expect(midnight.hour).toBe(0);
141
+ expect(noon.hour).toBe(12);
142
+ });
143
+
144
+ it('should return null for invalid hour or minute values', () => {
145
+ expect(parseDate('03/15/2024 13:00 PM')).toBeNull();
146
+ expect(parseDate('03/15/2024 09:60 AM')).toBeNull();
147
+ });
148
+ });
149
+
150
+ describe('validateDateString', () => {
151
+ it('should return true for empty string', () => {
152
+ expect(validateDateString({ text: '' })).toBe(true);
153
+ });
154
+
155
+ it('should return false for invalid format', () => {
156
+ expect(validateDateString({ text: 'not a date' })).toBe(false);
157
+ expect(validateDateString({ text: 'not a date', withTime: true })).toBe(false);
158
+ });
159
+
160
+ it('should return false when date is before min', () => {
161
+ const min = createDateValue(2024, 3, 1);
162
+
163
+ expect(validateDateString({ text: '02/28/2024', min })).toBe(false);
164
+ });
165
+
166
+ it('should return false when date is after max', () => {
167
+ const max = createDateValue(2024, 3, 31);
168
+
169
+ expect(validateDateString({ text: '04/01/2024', max })).toBe(false);
170
+ });
171
+
172
+ it('should return true when date is within bounds', () => {
173
+ const min = createDateValue(2024, 3, 1);
174
+ const max = createDateValue(2024, 3, 31);
175
+
176
+ expect(validateDateString({ text: '03/15/2024', min, max })).toBe(true);
177
+ });
178
+ });
179
+
180
+ describe('isSameDay', () => {
181
+ it('should return false for null or undefined inputs', () => {
182
+ const date = createDateValue(2024, 3, 15);
183
+ expect(isSameDay(null, date)).toBe(false);
184
+ expect(isSameDay(date, null)).toBe(false);
185
+ expect(isSameDay(null, null)).toBe(false);
186
+ });
187
+
188
+ it('should return true when dates have same year/month/day', () => {
189
+ const date1 = createDateValue(2024, 3, 15);
190
+ const date2 = createDateValue(2024, 3, 15);
191
+ expect(isSameDay(date1, date2)).toBe(true);
192
+ });
193
+
194
+ it('should return false when dates differ', () => {
195
+ const date1 = createDateValue(2024, 3, 15);
196
+ expect(isSameDay(date1, createDateValue(2025, 3, 15))).toBe(false);
197
+ expect(isSameDay(date1, createDateValue(2024, 4, 15))).toBe(false);
198
+ expect(isSameDay(date1, createDateValue(2024, 3, 16))).toBe(false);
199
+ });
200
+
201
+ it('should ignore time components', () => {
202
+ const date1 = createDateValue(2024, 3, 15, 9, 0);
203
+ const date2 = createDateValue(2024, 3, 15, 17, 30);
204
+ expect(isSameDay(date1, date2)).toBe(true);
205
+ });
206
+ });
207
+
208
+ describe('round-trip conversion', () => {
209
+ it('should preserve date through Date → DateValue → Date conversion', () => {
210
+ const original = createDate(2024, 3, 15, 14, 30);
211
+ const dateValue = toIntlDate(original);
212
+ const result = fromIntlDate(dateValue);
213
+
214
+ expect(result.getFullYear()).toBe(original.getFullYear());
215
+ expect(result.getMonth()).toBe(original.getMonth());
216
+ expect(result.getDate()).toBe(original.getDate());
217
+ expect(result.getHours()).toBe(original.getHours());
218
+ expect(result.getMinutes()).toBe(original.getMinutes());
219
+ });
220
+
221
+ it('should preserve date through string → DateValue → string conversion', () => {
222
+ expect(formatDate(parseDate('03/15/2024'))).toBe('03/15/2024');
223
+ expect(formatDate(parseDate('03/15/2024 02:30 PM'), true)).toBe('03/15/2024, 02:30 PM');
224
+ });
225
+ });
226
+ });
@@ -0,0 +1,2 @@
1
+ export { default as DsDatePicker } from './ds-date-picker';
2
+ export type { DsDatePickerProps, DsDatePickerLocale, DsDatePickerSlotProps } from './ds-date-picker.types';
@@ -0,0 +1,20 @@
1
+ .root {
2
+ display: flex;
3
+ gap: var(--spacing-xs);
4
+ }
5
+
6
+ .horizontal {
7
+ align-items: flex-end;
8
+ }
9
+
10
+ .vertical {
11
+ flex-direction: column;
12
+ }
13
+
14
+ .clearButton {
15
+ align-self: flex-end;
16
+ }
17
+
18
+ .formControl {
19
+ flex: 1;
20
+ }