gbs-add-block 1.2.14 → 1.2.16

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 (169) hide show
  1. package/README.md +2 -2
  2. package/index.cjs +219 -21
  3. package/package.json +1 -1
  4. package/source/beta-components/breadcrumb/README.md +34 -0
  5. package/source/beta-components/breadcrumb/__tests__/core.test.ts +58 -0
  6. package/source/beta-components/breadcrumb/core/index.ts +4 -0
  7. package/source/beta-components/breadcrumb/core/trail.ts +42 -0
  8. package/source/beta-components/breadcrumb/core/types.ts +20 -0
  9. package/source/beta-components/breadcrumb/index.ts +5 -0
  10. package/source/beta-components/breadcrumb/react/Breadcrumb.tsx +177 -0
  11. package/source/beta-components/breadcrumb/react/locale.ts +6 -0
  12. package/source/beta-components/breadcrumb/react/props.ts +14 -0
  13. package/source/beta-components/breadcrumb/styles.css +139 -0
  14. package/source/beta-components/button/README.md +32 -0
  15. package/source/beta-components/button/__tests__/core.test.ts +31 -0
  16. package/source/beta-components/button/core/index.ts +3 -0
  17. package/source/beta-components/button/core/state.ts +16 -0
  18. package/source/beta-components/button/core/types.ts +28 -0
  19. package/source/beta-components/button/index.ts +5 -0
  20. package/source/beta-components/button/react/Button.tsx +173 -0
  21. package/source/beta-components/button/react/locale.ts +5 -0
  22. package/source/beta-components/button/react/props.ts +23 -0
  23. package/source/beta-components/button/styles.css +233 -0
  24. package/source/beta-components/checkbox/README.md +36 -0
  25. package/source/beta-components/checkbox/__tests__/core.test.ts +40 -0
  26. package/source/beta-components/checkbox/core/group.ts +30 -0
  27. package/source/beta-components/checkbox/core/index.ts +3 -0
  28. package/source/beta-components/checkbox/core/types.ts +17 -0
  29. package/source/beta-components/checkbox/index.ts +9 -0
  30. package/source/beta-components/checkbox/react/Checkbox.tsx +147 -0
  31. package/source/beta-components/checkbox/react/CheckboxGroup.tsx +149 -0
  32. package/source/beta-components/checkbox/react/context.ts +16 -0
  33. package/source/beta-components/checkbox/react/locale.ts +5 -0
  34. package/source/beta-components/checkbox/react/props.ts +6 -0
  35. package/source/beta-components/checkbox/styles.css +212 -0
  36. package/source/beta-components/combobox/README.md +3 -3
  37. package/source/beta-components/combobox/react/MultiSelect.tsx +265 -263
  38. package/source/beta-components/combobox/react/Popover.tsx +38 -70
  39. package/source/beta-components/combobox/react/Select.tsx +200 -198
  40. package/source/beta-components/combobox/react/icons.tsx +6 -29
  41. package/source/beta-components/combobox/react/props.ts +4 -2
  42. package/source/beta-components/combobox/styles.css +18 -18
  43. package/source/beta-components/data-grid/react/Popover.tsx +35 -0
  44. package/source/beta-components/{datagrid → data-grid}/react/context.ts +2 -2
  45. package/source/beta-components/{datagrid → data-grid}/react/icons.tsx +8 -35
  46. package/source/beta-components/{datagrid → data-grid}/styles.css +24 -24
  47. package/source/beta-components/{datepicker → date-picker}/README.md +3 -3
  48. package/source/beta-components/{datepicker → date-picker}/react/DatePicker.tsx +249 -249
  49. package/source/beta-components/{datepicker → date-picker}/react/DateRangePicker.tsx +291 -291
  50. package/source/beta-components/date-picker/react/Popover.tsx +45 -0
  51. package/source/beta-components/date-picker/react/icons.tsx +15 -0
  52. package/source/beta-components/{datepicker → date-picker}/react/props.ts +2 -2
  53. package/source/beta-components/{datepicker → date-picker}/styles.css +18 -18
  54. package/source/beta-components/dialog/react/icons.tsx +16 -47
  55. package/source/beta-components/dialog/react/props.ts +2 -2
  56. package/source/beta-components/dialog/styles.css +21 -21
  57. package/source/beta-components/{fileuploader → file-uploader}/react/FileUploader.tsx +511 -515
  58. package/source/beta-components/{fileuploader → file-uploader}/react/icons.tsx +7 -27
  59. package/source/beta-components/{fileuploader → file-uploader}/react/props.ts +2 -2
  60. package/source/beta-components/{fileuploader → file-uploader}/styles.css +17 -17
  61. package/source/beta-components/input/core/count.ts +3 -12
  62. package/source/beta-components/input/react/Input.tsx +219 -219
  63. package/source/beta-components/input/react/OtpInput.tsx +256 -256
  64. package/source/beta-components/input/react/icons.tsx +3 -21
  65. package/source/beta-components/input/react/props.ts +2 -2
  66. package/source/beta-components/input/styles.css +15 -15
  67. package/source/beta-components/modal/react/Modal.tsx +242 -238
  68. package/source/beta-components/modal/react/icons.tsx +1 -19
  69. package/source/beta-components/modal/react/props.ts +2 -2
  70. package/source/beta-components/modal/styles.css +17 -13
  71. package/source/beta-components/shared/README.md +54 -0
  72. package/source/beta-components/shared/__tests__/boundaries.test.ts +95 -0
  73. package/source/beta-components/shared/__tests__/core.test.ts +55 -0
  74. package/source/beta-components/shared/core/cx.ts +9 -0
  75. package/source/beta-components/shared/core/field.ts +35 -0
  76. package/source/beta-components/shared/core/text.ts +14 -0
  77. package/source/beta-components/shared/index.ts +7 -0
  78. package/source/beta-components/shared/react/Popover.tsx +147 -0
  79. package/source/beta-components/shared/react/icons.tsx +74 -0
  80. package/source/beta-components/shared/react/useControllableState.ts +42 -0
  81. package/source/beta-components/shared/version.json +4 -0
  82. package/source/beta-components/shared/version.ts +6 -0
  83. package/source/beta-components/spinner/README.md +29 -0
  84. package/source/beta-components/spinner/__tests__/core.test.ts +48 -0
  85. package/source/beta-components/spinner/core/index.ts +3 -0
  86. package/source/beta-components/spinner/core/types.ts +28 -0
  87. package/source/beta-components/spinner/core/visibility.ts +34 -0
  88. package/source/beta-components/spinner/index.ts +6 -0
  89. package/source/beta-components/spinner/react/Spinner.tsx +96 -0
  90. package/source/beta-components/spinner/react/locale.ts +5 -0
  91. package/source/beta-components/spinner/react/props.ts +4 -0
  92. package/source/beta-components/spinner/react/useDelayedLoading.ts +39 -0
  93. package/source/beta-components/spinner/styles.css +167 -0
  94. package/source/beta-components/tabs/README.md +32 -0
  95. package/source/beta-components/tabs/__tests__/core.test.ts +51 -0
  96. package/source/beta-components/tabs/core/index.ts +3 -0
  97. package/source/beta-components/tabs/core/navigation.ts +54 -0
  98. package/source/beta-components/tabs/core/types.ts +16 -0
  99. package/source/beta-components/tabs/index.ts +5 -0
  100. package/source/beta-components/tabs/react/Tabs.tsx +258 -0
  101. package/source/beta-components/tabs/react/context.ts +21 -0
  102. package/source/beta-components/tabs/styles.css +257 -0
  103. package/source/beta-components/textarea/core/count.ts +3 -12
  104. package/source/beta-components/textarea/react/Textarea.tsx +208 -209
  105. package/source/beta-components/textarea/react/props.ts +2 -2
  106. package/source/beta-components/textarea/styles.css +12 -12
  107. package/source/beta-components/toaster/react/icons.tsx +19 -52
  108. package/source/beta-components/toaster/react/props.ts +2 -2
  109. package/source/beta-components/toaster/styles.css +18 -18
  110. package/source/beta-components/datagrid/react/Popover.tsx +0 -79
  111. package/source/beta-components/datepicker/react/Popover.tsx +0 -87
  112. package/source/beta-components/datepicker/react/icons.tsx +0 -32
  113. /package/source/beta-components/{datagrid → data-grid}/README.md +0 -0
  114. /package/source/beta-components/{datagrid → data-grid}/__tests__/core.test.ts +0 -0
  115. /package/source/beta-components/{datagrid → data-grid}/__tests__/export.test.ts +0 -0
  116. /package/source/beta-components/{datagrid → data-grid}/core/columnHelper.ts +0 -0
  117. /package/source/beta-components/{datagrid → data-grid}/core/columns.ts +0 -0
  118. /package/source/beta-components/{datagrid → data-grid}/core/filtering.ts +0 -0
  119. /package/source/beta-components/{datagrid → data-grid}/core/grid.ts +0 -0
  120. /package/source/beta-components/{datagrid → data-grid}/core/index.ts +0 -0
  121. /package/source/beta-components/{datagrid → data-grid}/core/rows.ts +0 -0
  122. /package/source/beta-components/{datagrid → data-grid}/core/sorting.ts +0 -0
  123. /package/source/beta-components/{datagrid → data-grid}/core/state.ts +0 -0
  124. /package/source/beta-components/{datagrid → data-grid}/core/store.ts +0 -0
  125. /package/source/beta-components/{datagrid → data-grid}/core/types.ts +0 -0
  126. /package/source/beta-components/{datagrid → data-grid}/core/values.ts +0 -0
  127. /package/source/beta-components/{datagrid → data-grid}/core/virtual.ts +0 -0
  128. /package/source/beta-components/{datagrid → data-grid}/export/csv.ts +0 -0
  129. /package/source/beta-components/{datagrid → data-grid}/export/download.ts +0 -0
  130. /package/source/beta-components/{datagrid → data-grid}/export/pdf.ts +0 -0
  131. /package/source/beta-components/{datagrid → data-grid}/export/table.ts +0 -0
  132. /package/source/beta-components/{datagrid → data-grid}/export/xlsx.ts +0 -0
  133. /package/source/beta-components/{datagrid → data-grid}/export/zip.ts +0 -0
  134. /package/source/beta-components/{datagrid → data-grid}/index.ts +0 -0
  135. /package/source/beta-components/{datagrid → data-grid}/react/Cell.tsx +0 -0
  136. /package/source/beta-components/{datagrid → data-grid}/react/ColumnMenu.tsx +0 -0
  137. /package/source/beta-components/{datagrid → data-grid}/react/DataGrid.tsx +0 -0
  138. /package/source/beta-components/{datagrid → data-grid}/react/FilterForm.tsx +0 -0
  139. /package/source/beta-components/{datagrid → data-grid}/react/HeaderRow.tsx +0 -0
  140. /package/source/beta-components/{datagrid → data-grid}/react/Pagination.tsx +0 -0
  141. /package/source/beta-components/{datagrid → data-grid}/react/Row.tsx +0 -0
  142. /package/source/beta-components/{datagrid → data-grid}/react/Toolbar.tsx +0 -0
  143. /package/source/beta-components/{datagrid → data-grid}/react/Viewport.tsx +0 -0
  144. /package/source/beta-components/{datagrid → data-grid}/react/hooks.ts +0 -0
  145. /package/source/beta-components/{datagrid → data-grid}/react/keyboard.ts +0 -0
  146. /package/source/beta-components/{datagrid → data-grid}/react/locale.ts +0 -0
  147. /package/source/beta-components/{datepicker → date-picker}/__tests__/core.test.ts +0 -0
  148. /package/source/beta-components/{datepicker → date-picker}/core/calendar.ts +0 -0
  149. /package/source/beta-components/{datepicker → date-picker}/core/format.ts +0 -0
  150. /package/source/beta-components/{datepicker → date-picker}/core/index.ts +0 -0
  151. /package/source/beta-components/{datepicker → date-picker}/core/types.ts +0 -0
  152. /package/source/beta-components/{datepicker → date-picker}/index.ts +0 -0
  153. /package/source/beta-components/{datepicker → date-picker}/react/Calendar.tsx +0 -0
  154. /package/source/beta-components/{datepicker → date-picker}/react/CalendarPanel.tsx +0 -0
  155. /package/source/beta-components/{datepicker → date-picker}/react/MonthYearPanels.tsx +0 -0
  156. /package/source/beta-components/{datepicker → date-picker}/react/locale.ts +0 -0
  157. /package/source/beta-components/{datepicker → date-picker}/react/useDatePicker.ts +0 -0
  158. /package/source/beta-components/{fileuploader → file-uploader}/README.md +0 -0
  159. /package/source/beta-components/{fileuploader → file-uploader}/__tests__/core.test.ts +0 -0
  160. /package/source/beta-components/{fileuploader → file-uploader}/core/format.ts +0 -0
  161. /package/source/beta-components/{fileuploader → file-uploader}/core/index.ts +0 -0
  162. /package/source/beta-components/{fileuploader → file-uploader}/core/store.ts +0 -0
  163. /package/source/beta-components/{fileuploader → file-uploader}/core/transport.ts +0 -0
  164. /package/source/beta-components/{fileuploader → file-uploader}/core/types.ts +0 -0
  165. /package/source/beta-components/{fileuploader → file-uploader}/core/validate.ts +0 -0
  166. /package/source/beta-components/{fileuploader → file-uploader}/index.ts +0 -0
  167. /package/source/beta-components/{fileuploader → file-uploader}/react/FileRow.tsx +0 -0
  168. /package/source/beta-components/{fileuploader → file-uploader}/react/locale.ts +0 -0
  169. /package/source/beta-components/{fileuploader → file-uploader}/react/useFileUploader.ts +0 -0
@@ -1,291 +1,291 @@
1
- "use client";
2
-
3
- import {
4
- useId,
5
- useImperativeHandle,
6
- useMemo,
7
- useRef,
8
- useState,
9
- type KeyboardEvent,
10
- type Ref,
11
- } from "react";
12
- import { toDate, toISODate } from "../core/calendar";
13
- import { inputPlaceholder } from "../core/format";
14
- import type { DateInput, DateRange } from "../core/types";
15
- import { CalendarPanel, type DatePreset } from "./CalendarPanel";
16
- import { CalendarIcon, XIcon } from "./icons";
17
- import { defaultDatePickerText } from "./locale";
18
- import { DatePickerPopover } from "./Popover";
19
- import { cx, type DatePickerHandle, type DatePickerSharedProps } from "./props";
20
- import { useDatePicker, type RangeField } from "./useDatePicker";
21
-
22
- /** A range as it comes in from props: each end may be a Date, an ISO string or a timestamp. */
23
- export interface DateRangeInput {
24
- start: DateInput | null;
25
- end: DateInput | null;
26
- }
27
-
28
- export interface DateRangePickerProps extends DatePickerSharedProps {
29
- value?: DateRangeInput | null;
30
- defaultValue?: DateRangeInput | null;
31
- /** `complete` is false on the first of the two clicks. */
32
- onChange?(range: DateRange, complete: boolean): void;
33
- /** Shortcuts shown beside the calendar ("Last 7 days"). */
34
- presets?: readonly DatePreset[];
35
- /** Close the calendar once both ends are chosen. Default true. */
36
- closeOnSelect?: boolean;
37
- ref?: Ref<DatePickerHandle<DateRange>>;
38
- }
39
-
40
- const EMPTY: DateRange = { start: null, end: null };
41
-
42
- const toRange = (input: DateRangeInput | null | undefined): DateRange => ({
43
- start: toDate(input?.start),
44
- end: toDate(input?.end),
45
- });
46
-
47
- /** Two dates in one field: pick the start, then the end, with a live preview between. */
48
- export function DateRangePicker(props: DateRangePickerProps) {
49
- const {
50
- ref,
51
- value,
52
- defaultValue,
53
- onChange,
54
- presets,
55
- closeOnSelect = true,
56
- min,
57
- max,
58
- isDateDisabled,
59
- locale,
60
- weekStartsOn,
61
- format,
62
- numberOfMonths = 2,
63
- showWeekNumbers = false,
64
- showToday = false,
65
- allowInput = true,
66
- fixedWeeks = true,
67
- label,
68
- description,
69
- error,
70
- placeholder,
71
- required = false,
72
- disabled = false,
73
- readOnly = false,
74
- clearable = true,
75
- size = "md",
76
- name,
77
- id: idProp,
78
- className,
79
- classNames,
80
- style,
81
- localeText,
82
- onOpenChange,
83
- } = props;
84
-
85
- const reactId = useId();
86
- const id = idProp ?? reactId;
87
- const popoverId = `${id}-popover`;
88
- const text = useMemo(() => ({ ...defaultDatePickerText, ...localeText }), [localeText]);
89
-
90
- const controlRef = useRef<HTMLDivElement>(null);
91
- const triggerRef = useRef<HTMLButtonElement>(null);
92
- const startRef = useRef<HTMLInputElement>(null);
93
-
94
- const minDate = useMemo(() => toDate(min), [min]);
95
- const maxDate = useMemo(() => toDate(max), [max]);
96
- const selection = useMemo<DateRange | undefined>(
97
- () => (value === undefined ? undefined : toRange(value)),
98
- [value],
99
- );
100
- // Read once, as for any uncontrolled input.
101
- const [initial] = useState<DateRange>(() => (defaultValue ? toRange(defaultValue) : EMPTY));
102
-
103
- const picker = useDatePicker({
104
- range: true,
105
- value: selection,
106
- defaultValue: initial,
107
- onSelectionChange: (next, complete) => onChange?.(next, complete),
108
- min: minDate,
109
- max: maxDate,
110
- isDateDisabled,
111
- locale,
112
- weekStartsOn,
113
- numberOfMonths,
114
- fixedWeeks,
115
- disabled,
116
- readOnly,
117
- closeOnSelect,
118
- format,
119
- text,
120
- controlRef,
121
- triggerRef,
122
- onOpenChange,
123
- });
124
-
125
- const { start, end } = picker.selection;
126
- const message = error ?? picker.inputError;
127
- const invalid = Boolean(message);
128
- const hasValue = start !== null || end !== null;
129
-
130
- useImperativeHandle(
131
- ref,
132
- () => ({
133
- open: () => picker.setOpen(true),
134
- close: () => picker.setOpen(false),
135
- toggle: () => picker.setOpen(!picker.open),
136
- focus: () => startRef.current?.focus({ preventScroll: true }),
137
- clear: picker.clear,
138
- getValue: () => picker.selection,
139
- }),
140
- [picker],
141
- );
142
-
143
- const onInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
144
- if (event.key === "ArrowDown" || (event.key === "Enter" && !picker.open)) {
145
- event.preventDefault();
146
- picker.setOpen(true);
147
- } else if (event.key === "Escape" && picker.open) {
148
- picker.setOpen(false);
149
- }
150
- };
151
-
152
- const describedBy =
153
- cx(description ? `${id}-description` : "", message ? `${id}-error` : "") || undefined;
154
-
155
- const field = (name: RangeField, fieldRef?: Ref<HTMLInputElement>) => (
156
- <input
157
- ref={fieldRef}
158
- id={`${id}-${name}`}
159
- className={cx("dp-input", classNames?.input)}
160
- value={picker.inputValue(name)}
161
- placeholder={placeholder ?? inputPlaceholder(locale)}
162
- disabled={disabled}
163
- readOnly={readOnly || !allowInput}
164
- required={required}
165
- autoComplete="off"
166
- spellCheck={false}
167
- aria-label={name === "start" ? text.startDate : text.endDate}
168
- aria-invalid={invalid || undefined}
169
- aria-describedby={describedBy}
170
- onChange={(event) => picker.onInputChange(name, event.target.value)}
171
- onBlur={() => picker.onInputBlur(name)}
172
- onKeyDown={onInputKeyDown}
173
- onClick={allowInput ? undefined : () => !disabled && picker.setOpen(true)}
174
- />
175
- );
176
-
177
- return (
178
- <div
179
- className={cx("dp-root", "dp-root-range", classNames?.root, className)}
180
- style={style}
181
- data-size={size}
182
- data-state={picker.open ? "open" : "closed"}
183
- data-invalid={invalid ? "" : undefined}
184
- >
185
- {label && (
186
- <span
187
- id={`${id}-label`}
188
- className={cx("dp-label", classNames?.label)}
189
- data-required={required || undefined}
190
- >
191
- {label}
192
- </span>
193
- )}
194
-
195
- <div
196
- ref={controlRef}
197
- className={cx("dp-control", classNames?.control)}
198
- role="group"
199
- aria-labelledby={label ? `${id}-label` : undefined}
200
- data-disabled={disabled || undefined}
201
- data-invalid={invalid ? "" : undefined}
202
- >
203
- {field("start", startRef)}
204
- <span className="dp-separator" aria-hidden="true">
205
- –
206
- </span>
207
- {field("end")}
208
-
209
- <span className="dp-actions">
210
- {clearable && hasValue && !disabled && !readOnly && (
211
- <button
212
- type="button"
213
- tabIndex={-1}
214
- className="dp-icon-button"
215
- aria-label={text.clear}
216
- onClick={picker.clear}
217
- >
218
- <XIcon width={14} height={14} />
219
- </button>
220
- )}
221
- <button
222
- ref={triggerRef}
223
- type="button"
224
- className="dp-icon-button dp-trigger"
225
- aria-label={text.openCalendar}
226
- aria-haspopup="dialog"
227
- aria-expanded={picker.open}
228
- aria-controls={picker.open ? popoverId : undefined}
229
- disabled={disabled}
230
- onPointerDown={picker.onTriggerPointerDown}
231
- onClick={picker.onTriggerClick}
232
- >
233
- <CalendarIcon />
234
- </button>
235
- </span>
236
- </div>
237
-
238
- {name && (
239
- <>
240
- <input type="hidden" name={`${name}-start`} value={start ? toISODate(start) : ""} />
241
- <input type="hidden" name={`${name}-end`} value={end ? toISODate(end) : ""} />
242
- </>
243
- )}
244
- {description && (
245
- <span id={`${id}-description`} className="dp-description">
246
- {description}
247
- </span>
248
- )}
249
- {message && (
250
- <span id={`${id}-error`} className="dp-error" role="alert">
251
- {message}
252
- </span>
253
- )}
254
-
255
- {picker.anchor && (
256
- <DatePickerPopover
257
- anchor={picker.anchor}
258
- onClose={picker.onPopoverClose}
259
- label={text.calendarLabel}
260
- id={popoverId}
261
- autoFocus={picker.gridFocus}
262
- className={classNames?.popover}
263
- >
264
- <CalendarPanel
265
- picker={picker}
266
- text={text}
267
- locale={locale}
268
- showWeekNumbers={showWeekNumbers}
269
- showToday={showToday}
270
- clearable={clearable}
271
- hasValue={hasValue}
272
- classNames={classNames}
273
- presets={presets}
274
- onPreset={(preset) => {
275
- picker.applyRange(toRange(preset.range));
276
- if (closeOnSelect) picker.closeAndFocusTrigger();
277
- }}
278
- />
279
- </DatePickerPopover>
280
- )}
281
-
282
- <span className="dp-sr-only" role="status">
283
- {start && end
284
- ? text.selectedRange(picker.formatValue(start), picker.formatValue(end))
285
- : start
286
- ? text.selectedDate(picker.formatValue(start))
287
- : ""}
288
- </span>
289
- </div>
290
- );
291
- }
1
+ "use client";
2
+
3
+ import {
4
+ useId,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type KeyboardEvent,
10
+ type Ref,
11
+ } from "react";
12
+ import { describeField } from "../../shared/core/field";
13
+ import { toDate, toISODate } from "../core/calendar";
14
+ import { inputPlaceholder } from "../core/format";
15
+ import type { DateInput, DateRange } from "../core/types";
16
+ import { CalendarPanel, type DatePreset } from "./CalendarPanel";
17
+ import { CalendarIcon, XIcon } from "./icons";
18
+ import { defaultDatePickerText } from "./locale";
19
+ import { DatePickerPopover } from "./Popover";
20
+ import { cx, type DatePickerHandle, type DatePickerSharedProps } from "./props";
21
+ import { useDatePicker, type RangeField } from "./useDatePicker";
22
+
23
+ /** A range as it comes in from props: each end may be a Date, an ISO string or a timestamp. */
24
+ export interface DateRangeInput {
25
+ start: DateInput | null;
26
+ end: DateInput | null;
27
+ }
28
+
29
+ export interface DateRangePickerProps extends DatePickerSharedProps {
30
+ value?: DateRangeInput | null;
31
+ defaultValue?: DateRangeInput | null;
32
+ /** `complete` is false on the first of the two clicks. */
33
+ onChange?(range: DateRange, complete: boolean): void;
34
+ /** Shortcuts shown beside the calendar ("Last 7 days"). */
35
+ presets?: readonly DatePreset[];
36
+ /** Close the calendar once both ends are chosen. Default true. */
37
+ closeOnSelect?: boolean;
38
+ ref?: Ref<DatePickerHandle<DateRange>>;
39
+ }
40
+
41
+ const EMPTY: DateRange = { start: null, end: null };
42
+
43
+ const toRange = (input: DateRangeInput | null | undefined): DateRange => ({
44
+ start: toDate(input?.start),
45
+ end: toDate(input?.end),
46
+ });
47
+
48
+ /** Two dates in one field: pick the start, then the end, with a live preview between. */
49
+ export function DateRangePicker(props: DateRangePickerProps) {
50
+ const {
51
+ ref,
52
+ value,
53
+ defaultValue,
54
+ onChange,
55
+ presets,
56
+ closeOnSelect = true,
57
+ min,
58
+ max,
59
+ isDateDisabled,
60
+ locale,
61
+ weekStartsOn,
62
+ format,
63
+ numberOfMonths = 2,
64
+ showWeekNumbers = false,
65
+ showToday = false,
66
+ allowInput = true,
67
+ fixedWeeks = true,
68
+ label,
69
+ description,
70
+ error,
71
+ placeholder,
72
+ required = false,
73
+ disabled = false,
74
+ readOnly = false,
75
+ clearable = true,
76
+ size = "md",
77
+ name,
78
+ id: idProp,
79
+ className,
80
+ classNames,
81
+ style,
82
+ localeText,
83
+ onOpenChange,
84
+ } = props;
85
+
86
+ const reactId = useId();
87
+ const id = idProp ?? reactId;
88
+ const popoverId = `${id}-popover`;
89
+ const text = useMemo(() => ({ ...defaultDatePickerText, ...localeText }), [localeText]);
90
+
91
+ const controlRef = useRef<HTMLDivElement>(null);
92
+ const triggerRef = useRef<HTMLButtonElement>(null);
93
+ const startRef = useRef<HTMLInputElement>(null);
94
+
95
+ const minDate = useMemo(() => toDate(min), [min]);
96
+ const maxDate = useMemo(() => toDate(max), [max]);
97
+ const selection = useMemo<DateRange | undefined>(
98
+ () => (value === undefined ? undefined : toRange(value)),
99
+ [value],
100
+ );
101
+ // Read once, as for any uncontrolled input.
102
+ const [initial] = useState<DateRange>(() => (defaultValue ? toRange(defaultValue) : EMPTY));
103
+
104
+ const picker = useDatePicker({
105
+ range: true,
106
+ value: selection,
107
+ defaultValue: initial,
108
+ onSelectionChange: (next, complete) => onChange?.(next, complete),
109
+ min: minDate,
110
+ max: maxDate,
111
+ isDateDisabled,
112
+ locale,
113
+ weekStartsOn,
114
+ numberOfMonths,
115
+ fixedWeeks,
116
+ disabled,
117
+ readOnly,
118
+ closeOnSelect,
119
+ format,
120
+ text,
121
+ controlRef,
122
+ triggerRef,
123
+ onOpenChange,
124
+ });
125
+
126
+ const { start, end } = picker.selection;
127
+ const message = error ?? picker.inputError;
128
+ const invalid = Boolean(message);
129
+ const hasValue = start !== null || end !== null;
130
+
131
+ useImperativeHandle(
132
+ ref,
133
+ () => ({
134
+ open: () => picker.setOpen(true),
135
+ close: () => picker.setOpen(false),
136
+ toggle: () => picker.setOpen(!picker.open),
137
+ focus: () => startRef.current?.focus({ preventScroll: true }),
138
+ clear: picker.clear,
139
+ getValue: () => picker.selection,
140
+ }),
141
+ [picker],
142
+ );
143
+
144
+ const onInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
145
+ if (event.key === "ArrowDown" || (event.key === "Enter" && !picker.open)) {
146
+ event.preventDefault();
147
+ picker.setOpen(true);
148
+ } else if (event.key === "Escape" && picker.open) {
149
+ picker.setOpen(false);
150
+ }
151
+ };
152
+
153
+ const describedBy = describeField(id, { description, error: message });
154
+
155
+ const field = (name: RangeField, fieldRef?: Ref<HTMLInputElement>) => (
156
+ <input
157
+ ref={fieldRef}
158
+ id={`${id}-${name}`}
159
+ className={cx("dp-input", classNames?.input)}
160
+ value={picker.inputValue(name)}
161
+ placeholder={placeholder ?? inputPlaceholder(locale)}
162
+ disabled={disabled}
163
+ readOnly={readOnly || !allowInput}
164
+ required={required}
165
+ autoComplete="off"
166
+ spellCheck={false}
167
+ aria-label={name === "start" ? text.startDate : text.endDate}
168
+ aria-invalid={invalid || undefined}
169
+ aria-describedby={describedBy}
170
+ onChange={(event) => picker.onInputChange(name, event.target.value)}
171
+ onBlur={() => picker.onInputBlur(name)}
172
+ onKeyDown={onInputKeyDown}
173
+ onClick={allowInput ? undefined : () => !disabled && picker.setOpen(true)}
174
+ />
175
+ );
176
+
177
+ return (
178
+ <div
179
+ className={cx("dp-root", "dp-root-range", classNames?.root, className)}
180
+ style={style}
181
+ data-size={size}
182
+ data-state={picker.open ? "open" : "closed"}
183
+ data-invalid={invalid ? "" : undefined}
184
+ >
185
+ {label && (
186
+ <span
187
+ id={`${id}-label`}
188
+ className={cx("dp-label", classNames?.label)}
189
+ data-required={required || undefined}
190
+ >
191
+ {label}
192
+ </span>
193
+ )}
194
+
195
+ <div
196
+ ref={controlRef}
197
+ className={cx("dp-control", classNames?.control)}
198
+ role="group"
199
+ aria-labelledby={label ? `${id}-label` : undefined}
200
+ data-disabled={disabled || undefined}
201
+ data-invalid={invalid ? "" : undefined}
202
+ >
203
+ {field("start", startRef)}
204
+ <span className="dp-separator" aria-hidden="true">
205
+ –
206
+ </span>
207
+ {field("end")}
208
+
209
+ <span className="dp-actions">
210
+ {clearable && hasValue && !disabled && !readOnly && (
211
+ <button
212
+ type="button"
213
+ tabIndex={-1}
214
+ className="dp-icon-button"
215
+ aria-label={text.clear}
216
+ onClick={picker.clear}
217
+ >
218
+ <XIcon width={14} height={14} />
219
+ </button>
220
+ )}
221
+ <button
222
+ ref={triggerRef}
223
+ type="button"
224
+ className="dp-icon-button dp-trigger"
225
+ aria-label={text.openCalendar}
226
+ aria-haspopup="dialog"
227
+ aria-expanded={picker.open}
228
+ aria-controls={picker.open ? popoverId : undefined}
229
+ disabled={disabled}
230
+ onPointerDown={picker.onTriggerPointerDown}
231
+ onClick={picker.onTriggerClick}
232
+ >
233
+ <CalendarIcon />
234
+ </button>
235
+ </span>
236
+ </div>
237
+
238
+ {name && (
239
+ <>
240
+ <input type="hidden" name={`${name}-start`} value={start ? toISODate(start) : ""} />
241
+ <input type="hidden" name={`${name}-end`} value={end ? toISODate(end) : ""} />
242
+ </>
243
+ )}
244
+ {description && (
245
+ <span id={`${id}-description`} className="dp-description">
246
+ {description}
247
+ </span>
248
+ )}
249
+ {message && (
250
+ <span id={`${id}-error`} className="dp-error" role="alert">
251
+ {message}
252
+ </span>
253
+ )}
254
+
255
+ {picker.anchor && (
256
+ <DatePickerPopover
257
+ anchor={picker.anchor}
258
+ onClose={picker.onPopoverClose}
259
+ label={text.calendarLabel}
260
+ id={popoverId}
261
+ autoFocus={picker.gridFocus}
262
+ className={classNames?.popover}
263
+ >
264
+ <CalendarPanel
265
+ picker={picker}
266
+ text={text}
267
+ locale={locale}
268
+ showWeekNumbers={showWeekNumbers}
269
+ showToday={showToday}
270
+ clearable={clearable}
271
+ hasValue={hasValue}
272
+ classNames={classNames}
273
+ presets={presets}
274
+ onPreset={(preset) => {
275
+ picker.applyRange(toRange(preset.range));
276
+ if (closeOnSelect) picker.closeAndFocusTrigger();
277
+ }}
278
+ />
279
+ </DatePickerPopover>
280
+ )}
281
+
282
+ <span className="dp-sr-only" role="status">
283
+ {start && end
284
+ ? text.selectedRange(picker.formatValue(start), picker.formatValue(end))
285
+ : start
286
+ ? text.selectedDate(picker.formatValue(start))
287
+ : ""}
288
+ </span>
289
+ </div>
290
+ );
291
+ }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { AnchoredPopover } from "../../shared/react/Popover";
5
+ import { cx } from "./props";
6
+
7
+ interface PopoverProps {
8
+ anchor: HTMLElement;
9
+ onClose(): void;
10
+ label: string;
11
+ id: string;
12
+ /** Move focus into the calendar when it opens. False while someone is typing. */
13
+ autoFocus?: boolean;
14
+ className?: string;
15
+ children: ReactNode;
16
+ }
17
+
18
+ /**
19
+ * The calendar panel. It repositions as the panel changes height between the
20
+ * day, month and year views, and it keeps its size rather than scrolling, so it
21
+ * only moves above the field when the whole panel fits there.
22
+ */
23
+ export function DatePickerPopover({
24
+ anchor,
25
+ onClose,
26
+ label,
27
+ id,
28
+ autoFocus = true,
29
+ className,
30
+ children,
31
+ }: PopoverProps) {
32
+ return (
33
+ <AnchoredPopover
34
+ anchor={anchor}
35
+ onClose={onClose}
36
+ label={label}
37
+ id={id}
38
+ className={cx("dp-popover", className)}
39
+ autoFocus={autoFocus && "[data-autofocus]"}
40
+ observeResize="self"
41
+ >
42
+ {children}
43
+ </AnchoredPopover>
44
+ );
45
+ }
@@ -0,0 +1,15 @@
1
+ import { Svg, type IconProps } from "../../shared/react/icons";
2
+
3
+ export {
4
+ ChevronDownIcon,
5
+ ChevronLeftIcon,
6
+ ChevronRightIcon,
7
+ XIcon,
8
+ } from "../../shared/react/icons";
9
+
10
+ export const CalendarIcon = (p: IconProps) => (
11
+ <Svg {...p}>
12
+ <rect x="3" y="5" width="18" height="16" rx="2" />
13
+ <path d="M3 10h18M8 3v4M16 3v4" />
14
+ </Svg>
15
+ );
@@ -72,5 +72,5 @@ export interface DatePickerSharedProps {
72
72
  onOpenChange?(open: boolean): void;
73
73
  }
74
74
 
75
- export const cx = (...names: (string | false | null | undefined)[]) =>
76
- names.filter(Boolean).join(" ");
75
+ // Shared with every other component, so class handling cannot drift.
76
+ export { cx } from "../../shared/core/cx";