@xsolla/xui-date-picker 0.218.1 → 0.219.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/README.md +39 -1
- package/native/index.d.mts +29 -0
- package/native/index.d.ts +29 -0
- package/native/index.js +58 -1
- package/native/index.js.map +1 -1
- package/native/index.mjs +58 -1
- package/native/index.mjs.map +1 -1
- package/package.json +6 -6
- package/web/index.d.mts +29 -0
- package/web/index.d.ts +29 -0
- package/web/index.js +61 -3
- package/web/index.js.map +1 -1
- package/web/index.mjs +61 -3
- package/web/index.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -95,11 +95,15 @@ const [date, setDate] = useState<Date | null>(null);
|
|
|
95
95
|
| `startDate` | `Date \| null` | — | Range start date. |
|
|
96
96
|
| `endDate` | `Date \| null` | — | Range end date. |
|
|
97
97
|
| `selectsRange` | `boolean` | `false` | Enable range selection. |
|
|
98
|
-
| `onChange` | `(date: Date \| DateRangeType) => void` | — | Fired when the date or range changes. |
|
|
98
|
+
| `onChange` | `(date: Date \| DateRangeType) => void` | — | Fired when the date or range changes. Range mode also reports an emptied range as `[null, null]`; single mode never reports "no date" here — see [Clearing](#clearing). |
|
|
99
|
+
| `label` | `string` | — | Label rendered above the field, in the same style as `Select`'s label. |
|
|
99
100
|
| `placeholder` | `string` | `'Choose a date'` / `'Choose a date range'` | Placeholder for the input, shown while it is empty. Not derived from `dateFormat`. |
|
|
100
101
|
| `dateFormat` | `string` | `'MM/dd/yyyy'` | `date-fns` format used to render and parse the field. Numeric tokens only — see [Date formats](#date-formats). |
|
|
101
102
|
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Input size. |
|
|
102
103
|
| `disabled` | `boolean` | `false` | Disable the input. |
|
|
104
|
+
| `readOnly` | `boolean` | `false` | The field can open the calendar but not be edited. |
|
|
105
|
+
| `clearable` | `boolean` | `false` | Show a clear (✕) control inside the field once it holds a value — see [Clearing](#clearing). |
|
|
106
|
+
| `onClear` | `() => void` | — | Fired after the `clearable` control empties the field. The only clear signal in single-date mode. |
|
|
103
107
|
| `backgroundColor` | `string` | — | Custom background colour for the input. |
|
|
104
108
|
| `dropdownPosition` | `'left' \| 'right'` | `'left'` | **Deprecated** — use the alignment suffix on `placement` (`dropdownPosition="right"` → `placement="bottom-end"`). Still honoured as a fallback when `placement` has no suffix. |
|
|
105
109
|
| `placement` | `DatePickerPlacement` | auto | Where the calendar opens, as `{side}[-alignment]` — same `{side}-{start\|center\|end}` convention as `ContextMenuPlacement`. A bare side aligns to `start`. When omitted, the calendar measures viewport space on open and auto-flips, preferring `bottom` → `top` → `left` → `right`, also flipping the cross-axis edge to stay on screen. When set, it always opens at that placement. Web only. |
|
|
@@ -124,6 +128,39 @@ const [date, setDate] = useState<Date | null>(null);
|
|
|
124
128
|
|
|
125
129
|
Inherits `ThemeOverrideProps` (`themeMode`, `themeProductContext`).
|
|
126
130
|
|
|
131
|
+
### Clearing
|
|
132
|
+
|
|
133
|
+
`clearable` adds a clear (✕) control inside the field, to the left of the calendar icon. It appears once the field holds a value and is suppressed while the field is `disabled` or `readOnly`.
|
|
134
|
+
|
|
135
|
+
**The clear signal differs by mode.** Range mode can express an empty value in `onChange` — it already reports half-picked ranges as `[start, end]` with nulls — so clearing fires `onChange([null, null])`. Single mode cannot: `onChange` takes a `Date`, and widening it to `Date | null` would break the handler signature of every existing consumer. Clearing a single date therefore fires **`onClear` only**. Wire `onClear` whenever you pass `clearable` in single mode, or the clear will not reach your state.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
const [date, setDate] = useState<Date | null>(new Date());
|
|
139
|
+
|
|
140
|
+
<DatePicker
|
|
141
|
+
clearable
|
|
142
|
+
label="Published date"
|
|
143
|
+
selectedDate={date}
|
|
144
|
+
onChange={(d) => setDate(d as Date)}
|
|
145
|
+
onClear={() => setDate(null)}
|
|
146
|
+
/>;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
const [range, setRange] = useState<DateRangeType>([null, null]);
|
|
151
|
+
|
|
152
|
+
<DatePicker
|
|
153
|
+
variant="dual"
|
|
154
|
+
clearable
|
|
155
|
+
label="Report period"
|
|
156
|
+
startDate={range[0]}
|
|
157
|
+
endDate={range[1]}
|
|
158
|
+
onChange={(value) => setRange(value as DateRangeType)}
|
|
159
|
+
/>;
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A cleared field stays empty until the props change or the user types again — blur does not put the previous value back. The same holds for a value you clear **from outside** while the calendar is open: the field adopts it on blur rather than re-emitting the date it was showing.
|
|
163
|
+
|
|
127
164
|
### Date formats
|
|
128
165
|
|
|
129
166
|
The field is edited segment by segment (type digits, arrow-step a segment, tab between them), so `dateFormat` supports **numeric tokens only** — exactly three, in any order, with anything between them treated as a literal separator:
|
|
@@ -338,3 +375,4 @@ const [date, setDate] = useState<Date | null>(null);
|
|
|
338
375
|
- Calendar dates are navigable via keyboard; selected dates are announced to screen readers.
|
|
339
376
|
- Focus is restored to the input when the calendar closes.
|
|
340
377
|
- Dates outside `minDate`/`maxDate` are disabled.
|
|
378
|
+
- The clear control is a real `button` with its own focus stop; clearing keeps focus on the field.
|
package/native/index.d.mts
CHANGED
|
@@ -38,12 +38,24 @@ interface DatePickerProps extends Omit<CalendarProps, "onChange"> {
|
|
|
38
38
|
* {@link DateRangeType} tuple whenever the picker is in range mode
|
|
39
39
|
* (`variant="dual"` or `selectsRange`). Either side of the tuple can be
|
|
40
40
|
* `null` while a range is only half-picked.
|
|
41
|
+
*
|
|
42
|
+
* Note the asymmetry when the field is emptied via {@link clearable}: range
|
|
43
|
+
* mode reports `[null, null]` here, but single mode has no `null` in this
|
|
44
|
+
* signature, so use {@link onClear} to observe a single-date clear.
|
|
41
45
|
*/
|
|
42
46
|
onChange?: (date: Date | DateRangeType) => void;
|
|
43
47
|
/**
|
|
44
48
|
* Placeholder text for the input
|
|
45
49
|
*/
|
|
46
50
|
placeholder?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Label rendered above the field, in the same style as `Select`'s label.
|
|
53
|
+
*
|
|
54
|
+
* Forwarded to the underlying `Input`. It used to work only by accident —
|
|
55
|
+
* unknown props were spread onto the field — which meant consumers had to
|
|
56
|
+
* cast to pass it, and it also leaked onto the calendar overlay (XUITSD-97).
|
|
57
|
+
*/
|
|
58
|
+
label?: string;
|
|
47
59
|
/** `date-fns` format for the field, defaulting to `"MM/dd/yyyy"`. Numeric `d`/`dd`, `M`/`MM` and `y`/`yyyy` tokens only, in any order (mind the casing — `mm` is minutes); anything else warns in development and falls back to the default. */
|
|
48
60
|
dateFormat?: string;
|
|
49
61
|
/**
|
|
@@ -67,6 +79,23 @@ interface DatePickerProps extends Omit<CalendarProps, "onChange"> {
|
|
|
67
79
|
* Optional leading icon shown inside the field.
|
|
68
80
|
*/
|
|
69
81
|
iconLeft?: ReactNode;
|
|
82
|
+
/**
|
|
83
|
+
* Shows a clear (✕) control inside the field, to the left of the calendar
|
|
84
|
+
* icon, whenever the field holds a value. Works in single and range mode,
|
|
85
|
+
* and is suppressed while the field is `disabled` or `readOnly`.
|
|
86
|
+
*
|
|
87
|
+
* Clearing empties the field, drops any active chip, and reports the new
|
|
88
|
+
* value: range mode fires `onChange([null, null])`; single mode fires
|
|
89
|
+
* {@link onClear} only, because `onChange` cannot express "no date". The
|
|
90
|
+
* cleared field is not reverted on blur — it stands until the props change.
|
|
91
|
+
*/
|
|
92
|
+
clearable?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Called when the field is emptied via the {@link clearable} control, after
|
|
95
|
+
* the field has been cleared. This is the only clear signal in single-date
|
|
96
|
+
* mode; in range mode `onChange([null, null])` fires as well.
|
|
97
|
+
*/
|
|
98
|
+
onClear?: () => void;
|
|
70
99
|
/**
|
|
71
100
|
* Highlights the field as invalid (error state).
|
|
72
101
|
*/
|
package/native/index.d.ts
CHANGED
|
@@ -38,12 +38,24 @@ interface DatePickerProps extends Omit<CalendarProps, "onChange"> {
|
|
|
38
38
|
* {@link DateRangeType} tuple whenever the picker is in range mode
|
|
39
39
|
* (`variant="dual"` or `selectsRange`). Either side of the tuple can be
|
|
40
40
|
* `null` while a range is only half-picked.
|
|
41
|
+
*
|
|
42
|
+
* Note the asymmetry when the field is emptied via {@link clearable}: range
|
|
43
|
+
* mode reports `[null, null]` here, but single mode has no `null` in this
|
|
44
|
+
* signature, so use {@link onClear} to observe a single-date clear.
|
|
41
45
|
*/
|
|
42
46
|
onChange?: (date: Date | DateRangeType) => void;
|
|
43
47
|
/**
|
|
44
48
|
* Placeholder text for the input
|
|
45
49
|
*/
|
|
46
50
|
placeholder?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Label rendered above the field, in the same style as `Select`'s label.
|
|
53
|
+
*
|
|
54
|
+
* Forwarded to the underlying `Input`. It used to work only by accident —
|
|
55
|
+
* unknown props were spread onto the field — which meant consumers had to
|
|
56
|
+
* cast to pass it, and it also leaked onto the calendar overlay (XUITSD-97).
|
|
57
|
+
*/
|
|
58
|
+
label?: string;
|
|
47
59
|
/** `date-fns` format for the field, defaulting to `"MM/dd/yyyy"`. Numeric `d`/`dd`, `M`/`MM` and `y`/`yyyy` tokens only, in any order (mind the casing — `mm` is minutes); anything else warns in development and falls back to the default. */
|
|
48
60
|
dateFormat?: string;
|
|
49
61
|
/**
|
|
@@ -67,6 +79,23 @@ interface DatePickerProps extends Omit<CalendarProps, "onChange"> {
|
|
|
67
79
|
* Optional leading icon shown inside the field.
|
|
68
80
|
*/
|
|
69
81
|
iconLeft?: ReactNode;
|
|
82
|
+
/**
|
|
83
|
+
* Shows a clear (✕) control inside the field, to the left of the calendar
|
|
84
|
+
* icon, whenever the field holds a value. Works in single and range mode,
|
|
85
|
+
* and is suppressed while the field is `disabled` or `readOnly`.
|
|
86
|
+
*
|
|
87
|
+
* Clearing empties the field, drops any active chip, and reports the new
|
|
88
|
+
* value: range mode fires `onChange([null, null])`; single mode fires
|
|
89
|
+
* {@link onClear} only, because `onChange` cannot express "no date". The
|
|
90
|
+
* cleared field is not reverted on blur — it stands until the props change.
|
|
91
|
+
*/
|
|
92
|
+
clearable?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Called when the field is emptied via the {@link clearable} control, after
|
|
95
|
+
* the field has been cleared. This is the only clear signal in single-date
|
|
96
|
+
* mode; in range mode `onChange([null, null])` fires as well.
|
|
97
|
+
*/
|
|
98
|
+
onClear?: () => void;
|
|
70
99
|
/**
|
|
71
100
|
* Highlights the field as invalid (error state).
|
|
72
101
|
*/
|
package/native/index.js
CHANGED
|
@@ -84,6 +84,7 @@ var toGapValue = (gap) => {
|
|
|
84
84
|
const asNumber = Number(gap);
|
|
85
85
|
return Number.isFinite(asNumber) ? asNumber : void 0;
|
|
86
86
|
};
|
|
87
|
+
var toNativeDisplay = (display) => display === "none" ? "none" : void 0;
|
|
87
88
|
var Box = ({
|
|
88
89
|
children,
|
|
89
90
|
onPress,
|
|
@@ -115,6 +116,7 @@ var Box = ({
|
|
|
115
116
|
marginBottom,
|
|
116
117
|
marginLeft,
|
|
117
118
|
marginRight,
|
|
119
|
+
display,
|
|
118
120
|
flexDirection,
|
|
119
121
|
flexWrap,
|
|
120
122
|
alignItems,
|
|
@@ -178,6 +180,7 @@ var Box = ({
|
|
|
178
180
|
marginBottom,
|
|
179
181
|
marginLeft,
|
|
180
182
|
marginRight,
|
|
183
|
+
display: toNativeDisplay(display),
|
|
181
184
|
flexDirection,
|
|
182
185
|
flexWrap,
|
|
183
186
|
alignItems,
|
|
@@ -638,6 +641,9 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
638
641
|
overlayThemeProductContext,
|
|
639
642
|
iconRight = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CalendarFieldIcon, {}),
|
|
640
643
|
iconLeft,
|
|
644
|
+
label,
|
|
645
|
+
clearable = false,
|
|
646
|
+
onClear,
|
|
641
647
|
error,
|
|
642
648
|
errorMessage,
|
|
643
649
|
readOnly,
|
|
@@ -659,6 +665,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
659
665
|
() => isRange ? [datePartsFromDate(startDate), datePartsFromDate(endDate)] : [datePartsFromDate(selectedDate)],
|
|
660
666
|
[isRange, startDate, endDate, selectedDate]
|
|
661
667
|
);
|
|
668
|
+
const propKey = `${selectedDate?.getTime() ?? ""}|${startDate?.getTime() ?? ""}|${endDate?.getTime() ?? ""}|${isRange}`;
|
|
662
669
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
663
670
|
const [groups, setGroups] = (0, import_react.useState)(propGroups);
|
|
664
671
|
const [edit, setEdit] = (0, import_react.useState)(null);
|
|
@@ -674,6 +681,15 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
674
681
|
const inputRef = (0, import_react.useRef)(null);
|
|
675
682
|
const chipSelectionRef = (0, import_react.useRef)(false);
|
|
676
683
|
const suppressReopenRef = (0, import_react.useRef)(false);
|
|
684
|
+
const sessionRef = (0, import_react.useRef)({
|
|
685
|
+
dirty: false,
|
|
686
|
+
cleared: false,
|
|
687
|
+
propKey
|
|
688
|
+
});
|
|
689
|
+
const markDirty = () => {
|
|
690
|
+
sessionRef.current.dirty = true;
|
|
691
|
+
sessionRef.current.cleared = false;
|
|
692
|
+
};
|
|
677
693
|
const activeChipValue = activeChip !== void 0 ? activeChip : internalChip;
|
|
678
694
|
const chipLabel = showChipLabel && activeChipValue && chips ? chips.find((c) => c.value === activeChipValue)?.label : void 0;
|
|
679
695
|
const isWithinBounds = (date) => {
|
|
@@ -733,6 +749,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
733
749
|
);
|
|
734
750
|
const showMask = hasAnyFilled || focused && isWeb;
|
|
735
751
|
const inputValue = chipLabel ?? (showMask ? composed.text : "");
|
|
752
|
+
const showClear = clearable && editable && (hasAnyFilled || !!chipLabel);
|
|
736
753
|
const orderedSegments = () => composeField().segments;
|
|
737
754
|
const firstSegmentKey = () => {
|
|
738
755
|
const first = orderedSegments()[0];
|
|
@@ -775,6 +792,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
775
792
|
if (start || end) applySelection([start, end], false);
|
|
776
793
|
};
|
|
777
794
|
const commitPart = (group, type, rawValue) => {
|
|
795
|
+
markDirty();
|
|
778
796
|
const clamped = clampDatePart(type, rawValue, groups[group]);
|
|
779
797
|
const next = groups.map((g, i) => {
|
|
780
798
|
if (i !== group) return g;
|
|
@@ -823,6 +841,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
823
841
|
setEdit(null);
|
|
824
842
|
if (committed !== null) advance(active);
|
|
825
843
|
} else {
|
|
844
|
+
markDirty();
|
|
826
845
|
setEdit({ group: active.group, type: active.type, buffer });
|
|
827
846
|
}
|
|
828
847
|
};
|
|
@@ -860,6 +879,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
860
879
|
e.preventDefault();
|
|
861
880
|
const active = activeSegment ?? firstSegmentKey();
|
|
862
881
|
if (!active) return;
|
|
882
|
+
markDirty();
|
|
863
883
|
const sameActive = edit && edit.group === active.group && edit.type === active.type;
|
|
864
884
|
if (sameActive && edit.buffer.length > 0) {
|
|
865
885
|
const buffer = edit.buffer.slice(0, -1);
|
|
@@ -911,6 +931,11 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
911
931
|
suppressReopenRef.current = false;
|
|
912
932
|
return;
|
|
913
933
|
}
|
|
934
|
+
sessionRef.current = {
|
|
935
|
+
dirty: false,
|
|
936
|
+
cleared: sessionRef.current.cleared,
|
|
937
|
+
propKey
|
|
938
|
+
};
|
|
914
939
|
setFocused(true);
|
|
915
940
|
setOpen(true);
|
|
916
941
|
if (isWeb && editable) setActiveSegment(firstSegmentKey());
|
|
@@ -927,7 +952,19 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
927
952
|
onBlurProp?.(e);
|
|
928
953
|
setFocused(false);
|
|
929
954
|
setActiveSegment(null);
|
|
955
|
+
if (sessionRef.current.propKey !== propKey && !sessionRef.current.dirty) {
|
|
956
|
+
sessionRef.current = { dirty: false, cleared: false, propKey };
|
|
957
|
+
setEdit(null);
|
|
958
|
+
setParseError(null);
|
|
959
|
+
setGroups(propGroups());
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
930
962
|
if (!editable) return;
|
|
963
|
+
if (sessionRef.current.cleared && !sessionRef.current.dirty) {
|
|
964
|
+
setEdit(null);
|
|
965
|
+
setParseError(null);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
931
968
|
let next = groups;
|
|
932
969
|
if (edit) {
|
|
933
970
|
const clamped = clampDatePart(
|
|
@@ -984,6 +1021,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
984
1021
|
setParseError("Date must be within the allowed range");
|
|
985
1022
|
return;
|
|
986
1023
|
}
|
|
1024
|
+
markDirty();
|
|
987
1025
|
setParseError(null);
|
|
988
1026
|
setEdit(null);
|
|
989
1027
|
setGroups(next);
|
|
@@ -994,13 +1032,29 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
994
1032
|
setInternalChip(value);
|
|
995
1033
|
onChipSelect?.(value);
|
|
996
1034
|
};
|
|
1035
|
+
const handleClear = () => {
|
|
1036
|
+
const next = isRange ? [datePartsFromDate(null), datePartsFromDate(null)] : [datePartsFromDate(null)];
|
|
1037
|
+
sessionRef.current.dirty = false;
|
|
1038
|
+
sessionRef.current.cleared = true;
|
|
1039
|
+
setGroups(next);
|
|
1040
|
+
setEdit(null);
|
|
1041
|
+
setParseError(null);
|
|
1042
|
+
setInternalChip(null);
|
|
1043
|
+
onChipSelect?.(null);
|
|
1044
|
+
if (isWeb && editable) setActiveSegment(firstSegmentKey());
|
|
1045
|
+
onClear?.();
|
|
1046
|
+
if (isRange) {
|
|
1047
|
+
const emptyRange = [null, null];
|
|
1048
|
+
onChange?.(emptyRange);
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
997
1051
|
const handleDateChange = (date) => applySelection(date, closeOnComplete);
|
|
998
1052
|
const closePanel = (0, import_react.useCallback)(() => setOpen(false), []);
|
|
999
1053
|
const renderTopContent = topContent ? () => topContent({ close: closePanel }) : void 0;
|
|
1000
1054
|
const renderBottomContent = bottomContent ? () => bottomContent({ close: closePanel }) : void 0;
|
|
1001
|
-
const propKey = `${selectedDate?.getTime() ?? ""}|${startDate?.getTime() ?? ""}|${endDate?.getTime() ?? ""}|${isRange}`;
|
|
1002
1055
|
(0, import_react.useEffect)(() => {
|
|
1003
1056
|
if (focused) return;
|
|
1057
|
+
sessionRef.current = { dirty: false, cleared: false, propKey };
|
|
1004
1058
|
setGroups(propGroups());
|
|
1005
1059
|
setEdit(null);
|
|
1006
1060
|
setParseError(null);
|
|
@@ -1177,6 +1231,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
1177
1231
|
...rest,
|
|
1178
1232
|
ref: inputRef,
|
|
1179
1233
|
size,
|
|
1234
|
+
label,
|
|
1180
1235
|
placeholder: placeholder || (isRange ? "Choose a date range" : "Choose a date"),
|
|
1181
1236
|
value: inputValue,
|
|
1182
1237
|
readOnly,
|
|
@@ -1187,6 +1242,8 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
1187
1242
|
onPaste: handlePaste,
|
|
1188
1243
|
iconRight,
|
|
1189
1244
|
iconLeft,
|
|
1245
|
+
extraClear: showClear,
|
|
1246
|
+
onRemove: handleClear,
|
|
1190
1247
|
error: error || !!parseError,
|
|
1191
1248
|
errorMessage: parseError ?? errorMessage,
|
|
1192
1249
|
backgroundColor,
|