@astryxdesign/core 0.4.3 → 0.4.4-canary.0e8af82
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/CHANGELOG.md +60 -0
- package/README.md +79 -45
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +13 -3
- package/dist/BottomSheet/BottomSheet.d.ts +75 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +396 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +63 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +441 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
- package/dist/BottomSheet/index.d.ts +11 -0
- package/dist/BottomSheet/index.d.ts.map +1 -0
- package/dist/BottomSheet/index.js +12 -0
- package/dist/BottomSheet/snapOffsets.d.ts +109 -0
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
- package/dist/BottomSheet/snapOffsets.js +206 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts +35 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +373 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +185 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +1038 -0
- package/dist/Calendar/Calendar.d.ts +17 -0
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +25 -1
- package/dist/Calendar/hooks/useCalendarConstraints.d.ts +22 -1
- package/dist/Calendar/hooks/useCalendarConstraints.d.ts.map +1 -1
- package/dist/Calendar/hooks/useCalendarConstraints.js +26 -4
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +9 -0
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +18 -7
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +15 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +38 -3
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +38 -3
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/Field/FieldLabel.d.ts +2 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +21 -3
- package/dist/Field/InputClearButton.d.ts +2 -1
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +6 -1
- package/dist/FormLayout/FormLayout.d.ts +24 -2
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +7 -2
- package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
- package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
- package/dist/FormLayout/FormLayoutContext.js +19 -4
- package/dist/FormLayout/index.d.ts +1 -1
- package/dist/FormLayout/index.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +9 -3
- package/dist/MobileNav/MobileNav.d.ts +12 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +102 -30
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +27 -8
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +15 -2
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +12 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +20 -4
- package/dist/StatusDot/StatusDot.d.ts +43 -3
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +42 -5
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +14 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +6 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +6 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +14 -2
- package/dist/Tokenizer/Tokenizer.js +11 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +4 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +10 -1
- package/dist/astryx.css +21 -0
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +8 -1
- package/dist/hooks/useFocusTrap.d.ts +0 -11
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +1 -11
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +1 -0
- package/dist/hooks/useResolvedRequired.d.ts +19 -0
- package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
- package/dist/hooks/useResolvedRequired.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +22 -4
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/expandColorScale.d.ts +17 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +63 -33
- package/dist/utils/ime.d.ts +5 -0
- package/dist/utils/ime.d.ts.map +1 -0
- package/dist/utils/ime.js +63 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/plainDate.d.ts +6 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +12 -0
- package/locales/af-ZA.json +958 -0
- package/locales/ar-SA.json +998 -0
- package/locales/ca-ES.json +974 -0
- package/locales/cs-CZ.json +990 -0
- package/locales/da-DK.json +966 -0
- package/locales/de-DE.json +966 -0
- package/locales/el-GR.json +990 -0
- package/locales/en.json +52 -0
- package/locales/es-ES.json +978 -0
- package/locales/fi-FI.json +990 -0
- package/locales/fr-FR.json +928 -0
- package/locales/he-IL.json +990 -0
- package/locales/hu-HU.json +990 -0
- package/locales/it-IT.json +978 -0
- package/locales/ja-JP.json +998 -0
- package/locales/ko-KR.json +990 -0
- package/locales/nl-NL.json +954 -0
- package/locales/no-NO.json +978 -0
- package/locales/pl-PL.json +982 -0
- package/locales/pseudo.json +39 -0
- package/locales/pt-BR.json +982 -0
- package/locales/pt-PT.json +982 -0
- package/locales/ro-RO.json +982 -0
- package/locales/ru-RU.json +990 -0
- package/locales/sr-SP.json +990 -0
- package/locales/sv-SE.json +986 -0
- package/locales/tr-TR.json +990 -0
- package/locales/uk-UA.json +990 -0
- package/locales/vi-VN.json +986 -0
- package/locales/zh-CN.json +998 -0
- package/locales/zh-TW.json +998 -0
- package/package.json +9 -8
- package/src/Banner/Banner.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +40 -0
- package/src/Banner/Banner.tsx +27 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +299 -0
- package/src/BottomSheet/BottomSheet.test.tsx +2265 -0
- package/src/BottomSheet/BottomSheet.tsx +559 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +601 -0
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
- package/src/BottomSheet/index.ts +19 -0
- package/src/BottomSheet/snapOffsets.test.ts +254 -0
- package/src/BottomSheet/snapOffsets.ts +245 -0
- package/src/BottomSheet/useMobileKeyboard.ts +499 -0
- package/src/BottomSheet/useSheetGestures.test.ts +1112 -0
- package/src/BottomSheet/useSheetGestures.ts +1503 -0
- package/src/Calendar/Calendar.doc.mjs +16 -0
- package/src/Calendar/Calendar.test.tsx +96 -0
- package/src/Calendar/Calendar.tsx +55 -0
- package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/CheckboxInput/CheckboxInput.tsx +6 -0
- package/src/ComplexSelector/ComplexSelector.tsx +15 -6
- package/src/ContextMenu/ContextMenu.tsx +4 -2
- package/src/DateInput/DateInput.test.tsx +21 -0
- package/src/DateInput/DateInput.tsx +12 -2
- package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
- package/src/DateRangeInput/DateRangeInput.tsx +72 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
- package/src/DateTimeInput/DateTimeInput.tsx +43 -4
- package/src/Dialog/Dialog.tsx +2 -1
- package/src/Field/Field.doc.mjs +1 -0
- package/src/Field/FieldLabel.tsx +20 -4
- package/src/Field/InputClearButton.test.tsx +25 -0
- package/src/Field/InputClearButton.tsx +4 -1
- package/src/FileInput/FileInput.test.tsx +53 -9
- package/src/FormLayout/FormLayout.doc.mjs +13 -0
- package/src/FormLayout/FormLayout.test.tsx +181 -4
- package/src/FormLayout/FormLayout.tsx +33 -2
- package/src/FormLayout/FormLayoutContext.ts +22 -7
- package/src/FormLayout/index.ts +1 -1
- package/src/Lightbox/Lightbox.test.tsx +49 -28
- package/src/Lightbox/Lightbox.tsx +7 -3
- package/src/MobileNav/MobileNav.tsx +128 -37
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
- package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
- package/src/MultiSelector/MultiSelector.test.tsx +195 -51
- package/src/MultiSelector/MultiSelector.tsx +25 -9
- package/src/NumberInput/NumberInput.test.tsx +18 -0
- package/src/NumberInput/NumberInput.tsx +12 -2
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/RadioList/RadioList.tsx +9 -1
- package/src/Selector/Selector.test.tsx +85 -26
- package/src/Selector/Selector.tsx +16 -5
- package/src/StatusDot/StatusDot.doc.mjs +13 -0
- package/src/StatusDot/StatusDot.test.tsx +115 -8
- package/src/StatusDot/StatusDot.tsx +74 -9
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +13 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
- package/src/TextArea/TextArea.tsx +3 -1
- package/src/TextInput/TextInput.tsx +3 -1
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +11 -1
- package/src/Tokenizer/Tokenizer.test.tsx +50 -32
- package/src/Tokenizer/Tokenizer.tsx +5 -5
- package/src/Tooltip/useTooltip.tsx +4 -1
- package/src/Typeahead/BaseTypeahead.tsx +10 -3
- package/src/Typeahead/Typeahead.test.tsx +116 -7
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/stubMatchMedia.test.ts +79 -0
- package/src/__tests__/stubMatchMedia.ts +95 -0
- package/src/hooks/index.ts +9 -5
- package/src/hooks/useFocusTrap.ts +1 -14
- package/src/hooks/useInputStatusIcon.test.tsx +19 -0
- package/src/hooks/useInputStatusIcon.tsx +7 -0
- package/src/hooks/useResolvedRequired.ts +42 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.ts +22 -4
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/plainDate.test.ts +50 -0
- package/src/utils/plainDate.ts +12 -0
- package/dist/astryx.umd.js +0 -147
- package/dist/astryx.umd.js.map +0 -7
|
@@ -68,6 +68,18 @@ export const docs = {
|
|
|
68
68
|
type: 'Array<(date: Date) => boolean>',
|
|
69
69
|
description: 'Custom constraint functions.',
|
|
70
70
|
},
|
|
71
|
+
{
|
|
72
|
+
name: 'maxRangeSpan',
|
|
73
|
+
type: 'number',
|
|
74
|
+
description:
|
|
75
|
+
'Range mode: max days a range may span, both endpoints counted (7 = a 7-day window). Caps the window from the picked start; before a start is picked every day stays selectable.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'minRangeSpan',
|
|
79
|
+
type: 'number',
|
|
80
|
+
description:
|
|
81
|
+
'Range mode: min days a range must span, both endpoints counted (2 forbids a single-day range). Default 1.',
|
|
82
|
+
},
|
|
71
83
|
{
|
|
72
84
|
name: 'focusDate',
|
|
73
85
|
type: 'ISODateString',
|
|
@@ -142,6 +154,8 @@ export const docsZh = {
|
|
|
142
154
|
{name: 'min', type: 'ISODateString', description: '可选择的最早日期。'},
|
|
143
155
|
{name: 'max', type: 'ISODateString', description: '可选择的最晚日期。'},
|
|
144
156
|
{name: 'dateConstraints', type: 'Array<(date: Date) => boolean>', description: '自定义约束函数。'},
|
|
157
|
+
{name: 'maxRangeSpan', type: 'number', description: '范围模式:范围最多可跨越的天数,含首尾两端(7 = 7 天窗口)。选定起始日后限制窗口大小。'},
|
|
158
|
+
{name: 'minRangeSpan', type: 'number', description: '范围模式:范围最少需跨越的天数,含首尾两端(2 表示禁止单日范围)。默认为 1。'},
|
|
145
159
|
{name: 'focusDate', type: 'ISODateString', description: '受控可见月份。'},
|
|
146
160
|
{name: 'onFocusDateChange', type: '(focusDate: ISODateString) => void', description: '导航回调函数。'},
|
|
147
161
|
{name: 'handleRef', type: 'React.Ref<CalendarHandle>', description: '日历导航的命令式句柄,包括 navigateTo()。'},
|
|
@@ -205,6 +219,8 @@ export const docsDense = {
|
|
|
205
219
|
min: 'minimum selectable date',
|
|
206
220
|
max: 'maximum selectable date',
|
|
207
221
|
dateConstraints: 'custom constraint fns',
|
|
222
|
+
maxRangeSpan: 'range mode: max days a range may span, both ends counted (7 = 7-day window)',
|
|
223
|
+
minRangeSpan: 'range mode: min days a range must span, both ends counted (default 1)',
|
|
208
224
|
focusDate: 'controlled visible month',
|
|
209
225
|
onFocusDateChange: 'navigation callback',
|
|
210
226
|
handleRef: 'imperative navigation handle',
|
|
@@ -236,6 +236,102 @@ describe('Calendar', () => {
|
|
|
236
236
|
expect(sunday).toBeDisabled();
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
it('caps the end date to maxRangeSpan once a start is picked', async () => {
|
|
240
|
+
const user = userEvent.setup();
|
|
241
|
+
|
|
242
|
+
render(<Calendar mode="range" focusDate="2026-01-01" maxRangeSpan={7} />);
|
|
243
|
+
|
|
244
|
+
// Before a start is picked every day is selectable.
|
|
245
|
+
expect(getDayButton(20)).not.toBeDisabled();
|
|
246
|
+
|
|
247
|
+
// Pick Jan 10 as the start. A 7-day window spans Jan 4–16 (start ± 6).
|
|
248
|
+
await user.click(getDayButton(10));
|
|
249
|
+
|
|
250
|
+
expect(getDayButton(16)).not.toBeDisabled(); // 6 days after — the edge
|
|
251
|
+
expect(getDayButton(17)).toBeDisabled(); // 7 days after — beyond the cap
|
|
252
|
+
expect(getDayButton(4)).not.toBeDisabled(); // 6 days before — symmetric
|
|
253
|
+
expect(getDayButton(3)).toBeDisabled(); // 7 days before — beyond the cap
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('enforces minRangeSpan once a start is picked', async () => {
|
|
257
|
+
const user = userEvent.setup();
|
|
258
|
+
|
|
259
|
+
render(<Calendar mode="range" focusDate="2026-01-01" minRangeSpan={3} />);
|
|
260
|
+
|
|
261
|
+
// Pick Jan 10. A 3-day minimum forbids ends closer than 2 days away.
|
|
262
|
+
await user.click(getDayButton(10));
|
|
263
|
+
|
|
264
|
+
// The picked start itself stays enabled — it is the active selection
|
|
265
|
+
// anchor, not an unreachable end.
|
|
266
|
+
expect(getDayButton(10)).not.toBeDisabled();
|
|
267
|
+
expect(getDayButton(11)).toBeDisabled(); // 1 day apart — too short
|
|
268
|
+
expect(getDayButton(12)).not.toBeDisabled(); // 3-day span — allowed
|
|
269
|
+
expect(getDayButton(8)).not.toBeDisabled(); // 3-day span the other way
|
|
270
|
+
expect(getDayButton(9)).toBeDisabled(); // 2-day span — too short
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('clears the in-progress start when the anchor is clicked again', async () => {
|
|
274
|
+
const user = userEvent.setup();
|
|
275
|
+
const handleChange = vi.fn();
|
|
276
|
+
|
|
277
|
+
render(
|
|
278
|
+
<Calendar
|
|
279
|
+
mode="range"
|
|
280
|
+
focusDate="2026-01-01"
|
|
281
|
+
onChange={handleChange}
|
|
282
|
+
minRangeSpan={3}
|
|
283
|
+
/>,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Pick Jan 10 as the start, then click it again.
|
|
287
|
+
await user.click(getDayButton(10));
|
|
288
|
+
await user.click(getDayButton(10));
|
|
289
|
+
|
|
290
|
+
// No zero-length range is committed…
|
|
291
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
292
|
+
|
|
293
|
+
// …and the start is cleared: the days that minRangeSpan disabled around
|
|
294
|
+
// the old anchor are selectable again, so a new start can be placed there.
|
|
295
|
+
expect(getDayButton(11)).not.toBeDisabled();
|
|
296
|
+
expect(getDayButton(9)).not.toBeDisabled();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('does not apply range-span constraints in single mode', async () => {
|
|
300
|
+
const user = userEvent.setup();
|
|
301
|
+
|
|
302
|
+
render(<Calendar focusDate="2026-01-01" maxRangeSpan={7} />);
|
|
303
|
+
|
|
304
|
+
// A picked single date must not disable far-away days.
|
|
305
|
+
await user.click(getDayButton(10));
|
|
306
|
+
expect(getDayButton(25)).not.toBeDisabled();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('keeps a controlled value that is wider than maxRangeSpan (selection-only)', () => {
|
|
310
|
+
// The span constraint governs which days are pickable, not validation of
|
|
311
|
+
// an already-set value — so a 15-day value under a 7-day cap stays
|
|
312
|
+
// rendered and is never silently rewritten.
|
|
313
|
+
render(
|
|
314
|
+
<Calendar
|
|
315
|
+
mode="range"
|
|
316
|
+
value={{start: '2026-01-05', end: '2026-01-20'}}
|
|
317
|
+
focusDate="2026-01-01"
|
|
318
|
+
maxRangeSpan={7}
|
|
319
|
+
/>,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// No selection is in progress (both endpoints are set), so no anchor →
|
|
323
|
+
// every day is pickable and both endpoints render selected.
|
|
324
|
+
expect(getDayButton(20)).not.toBeDisabled();
|
|
325
|
+
expect(getDayButton(5).closest('[role="gridcell"]')).toHaveAttribute(
|
|
326
|
+
'aria-selected',
|
|
327
|
+
'true',
|
|
328
|
+
);
|
|
329
|
+
expect(getDayButton(20).closest('[role="gridcell"]')).toHaveAttribute(
|
|
330
|
+
'aria-selected',
|
|
331
|
+
'true',
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
239
335
|
// ─── Multi-Month ─────────────────────────────────────────────
|
|
240
336
|
|
|
241
337
|
it('renders two months when numberOfMonths={2}', () => {
|
|
@@ -120,6 +120,25 @@ interface CalendarBaseProps extends Omit<
|
|
|
120
120
|
*/
|
|
121
121
|
dateConstraints?: ReadonlyArray<(date: Date) => boolean>;
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Range mode only. Maximum number of days a selected range may span,
|
|
125
|
+
* counting both endpoints — `maxRangeSpan={7}` allows a 7-day window
|
|
126
|
+
* (start + 6 days). Once a start date is picked, days beyond this distance
|
|
127
|
+
* from it are disabled in either direction; before a start is picked every
|
|
128
|
+
* otherwise-valid day stays selectable. Use for rolling windows like "at
|
|
129
|
+
* most a week from the chosen day". For fixed calendar bounds use min/max.
|
|
130
|
+
*/
|
|
131
|
+
maxRangeSpan?: number;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Range mode only. Minimum number of days a selected range must span,
|
|
135
|
+
* counting both endpoints — `minRangeSpan={2}` forbids a single-day range.
|
|
136
|
+
* Once a start date is picked, days closer than this to it are disabled —
|
|
137
|
+
* except the start itself, which stays selectable as the active anchor.
|
|
138
|
+
* Defaults to 1 (a same-day start and end is allowed).
|
|
139
|
+
*/
|
|
140
|
+
minRangeSpan?: number;
|
|
141
|
+
|
|
123
142
|
/**
|
|
124
143
|
* Controlled focus date (which month is visible).
|
|
125
144
|
* If not provided, defaults to selected date or today.
|
|
@@ -211,6 +230,8 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
211
230
|
min,
|
|
212
231
|
max,
|
|
213
232
|
dateConstraints,
|
|
233
|
+
maxRangeSpan,
|
|
234
|
+
minRangeSpan,
|
|
214
235
|
focusDate: focusDateProp,
|
|
215
236
|
onFocusDateChange,
|
|
216
237
|
hasOutsideDays = true,
|
|
@@ -425,6 +446,23 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
425
446
|
} else {
|
|
426
447
|
// Second click - complete the range
|
|
427
448
|
const startPd = plainDateFromISO(rangeSelectionStart);
|
|
449
|
+
|
|
450
|
+
// Clicking the anchor again clears the in-progress start rather than
|
|
451
|
+
// committing a zero-length range. This is also the escape hatch when
|
|
452
|
+
// `minRangeSpan` disables the days around the anchor: without it the
|
|
453
|
+
// anchor would be the only clickable day left and the start could
|
|
454
|
+
// never be moved. `minRangeSpan` leaves the anchor itself enabled
|
|
455
|
+
// precisely so this toggle stays reachable.
|
|
456
|
+
if (plainDateIsEqual(date, startPd)) {
|
|
457
|
+
setRangeSelectionStart(null);
|
|
458
|
+
announce(
|
|
459
|
+
t('@astryx.calendar.rangeClearedAnnounce', {
|
|
460
|
+
date: plainDateFormat(date, DATE_FORMAT_WITH_WEEKDAY),
|
|
461
|
+
}),
|
|
462
|
+
);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
428
466
|
let start: ISODateString;
|
|
429
467
|
let end: ISODateString;
|
|
430
468
|
|
|
@@ -528,6 +566,8 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
528
566
|
min={min}
|
|
529
567
|
max={max}
|
|
530
568
|
dateConstraints={dateConstraints}
|
|
569
|
+
maxRangeSpan={maxRangeSpan}
|
|
570
|
+
minRangeSpan={minRangeSpan}
|
|
531
571
|
hasOutsideDays={hasOutsideDays}
|
|
532
572
|
hasWeekNumbers={hasWeekNumbers}
|
|
533
573
|
hasVariableRowCount={hasVariableRowCount}
|
|
@@ -567,6 +607,8 @@ interface MonthGridProps {
|
|
|
567
607
|
min?: ISODateString;
|
|
568
608
|
max?: ISODateString;
|
|
569
609
|
dateConstraints?: ReadonlyArray<(date: Date) => boolean>;
|
|
610
|
+
maxRangeSpan?: number;
|
|
611
|
+
minRangeSpan?: number;
|
|
570
612
|
hasOutsideDays: boolean;
|
|
571
613
|
hasWeekNumbers: boolean;
|
|
572
614
|
hasVariableRowCount: boolean;
|
|
@@ -589,6 +631,8 @@ function MonthGrid({
|
|
|
589
631
|
min,
|
|
590
632
|
max,
|
|
591
633
|
dateConstraints,
|
|
634
|
+
maxRangeSpan,
|
|
635
|
+
minRangeSpan,
|
|
592
636
|
hasOutsideDays,
|
|
593
637
|
hasWeekNumbers,
|
|
594
638
|
hasVariableRowCount,
|
|
@@ -611,10 +655,21 @@ function MonthGrid({
|
|
|
611
655
|
hasVariableRowCount,
|
|
612
656
|
});
|
|
613
657
|
|
|
658
|
+
const rangeAnchor = useMemo(
|
|
659
|
+
() =>
|
|
660
|
+
mode === 'range' && rangeSelectionStart
|
|
661
|
+
? plainDateFromISO(rangeSelectionStart)
|
|
662
|
+
: null,
|
|
663
|
+
[mode, rangeSelectionStart],
|
|
664
|
+
);
|
|
665
|
+
|
|
614
666
|
const {isDateDisabled} = useCalendarConstraints({
|
|
615
667
|
min,
|
|
616
668
|
max,
|
|
617
669
|
dateConstraints,
|
|
670
|
+
maxRangeSpan,
|
|
671
|
+
minRangeSpan,
|
|
672
|
+
rangeAnchor,
|
|
618
673
|
});
|
|
619
674
|
|
|
620
675
|
// Parse selected date for roving tabindex priority
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
plainDateToDate,
|
|
21
21
|
plainDateIsBefore,
|
|
22
22
|
plainDateIsAfter,
|
|
23
|
+
plainDateDiffDays,
|
|
23
24
|
} from '../../utils/plainDate';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -35,6 +36,25 @@ export interface UseCalendarConstraintsOptions {
|
|
|
35
36
|
* Date is disabled if ANY function returns false.
|
|
36
37
|
*/
|
|
37
38
|
dateConstraints?: ReadonlyArray<(date: Date) => boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Maximum number of days a range may span, counting both endpoints
|
|
41
|
+
* (a value of 7 allows a 7-day window). In range mode, once a start is
|
|
42
|
+
* picked, dates further than this from it are disabled. No effect until a
|
|
43
|
+
* range anchor exists.
|
|
44
|
+
*/
|
|
45
|
+
maxRangeSpan?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Minimum number of days a range must span, counting both endpoints
|
|
48
|
+
* (a value of 2 forbids a single-day range). In range mode, once a start
|
|
49
|
+
* is picked, dates closer than this to it are disabled.
|
|
50
|
+
*/
|
|
51
|
+
minRangeSpan?: number;
|
|
52
|
+
/**
|
|
53
|
+
* The in-progress range start (first click, awaiting the second). Span
|
|
54
|
+
* constraints are measured from this date. Null when no selection is
|
|
55
|
+
* underway.
|
|
56
|
+
*/
|
|
57
|
+
rangeAnchor?: PlainDate | null;
|
|
38
58
|
}
|
|
39
59
|
|
|
40
60
|
/**
|
|
@@ -53,13 +73,15 @@ export interface UseCalendarConstraintsReturn {
|
|
|
53
73
|
* Hook for managing calendar date validation constraints.
|
|
54
74
|
*
|
|
55
75
|
* Provides a function to check if a date is disabled based on
|
|
56
|
-
* min/max bounds and custom constraint functions.
|
|
76
|
+
* min/max bounds, range-span bounds, and custom constraint functions.
|
|
57
77
|
*
|
|
58
78
|
* @example
|
|
59
79
|
* ```
|
|
60
80
|
* const {isDateDisabled} = useCalendarConstraints({
|
|
61
81
|
* min: '2026-01-01',
|
|
62
82
|
* max: '2026-12-31',
|
|
83
|
+
* maxRangeSpan: 7, // once a start is picked, cap the window at 7 days
|
|
84
|
+
* rangeAnchor, // the in-progress start (null before the first click)
|
|
63
85
|
* dateConstraints: [
|
|
64
86
|
* (date) => date.getDay() !== 0, // No Sundays (receives native Date)
|
|
65
87
|
* ],
|
|
@@ -74,7 +96,8 @@ export interface UseCalendarConstraintsReturn {
|
|
|
74
96
|
export function useCalendarConstraints(
|
|
75
97
|
options: UseCalendarConstraintsOptions,
|
|
76
98
|
): UseCalendarConstraintsReturn {
|
|
77
|
-
const {min, max, dateConstraints} =
|
|
99
|
+
const {min, max, dateConstraints, maxRangeSpan, minRangeSpan, rangeAnchor} =
|
|
100
|
+
options;
|
|
78
101
|
|
|
79
102
|
// Parse min/max dates
|
|
80
103
|
const minDate = useMemo(() => (min ? plainDateFromISO(min) : null), [min]);
|
|
@@ -93,6 +116,27 @@ export function useCalendarConstraints(
|
|
|
93
116
|
return true;
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
// Range-span bounds, measured from the in-progress start. Spans count
|
|
120
|
+
// both endpoints (a span of 7 spans a 7-day window), so the reachable
|
|
121
|
+
// distance from the anchor is `span - 1` days in either direction. Only
|
|
122
|
+
// active once a start is picked — before that, every day stays pickable.
|
|
123
|
+
if (rangeAnchor) {
|
|
124
|
+
const distance = Math.abs(plainDateDiffDays(rangeAnchor, date));
|
|
125
|
+
if (maxRangeSpan != null && distance > maxRangeSpan - 1) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
// The anchor itself (distance 0) is never disabled by minRangeSpan —
|
|
129
|
+
// it is the picked start, and disabling it would render the active
|
|
130
|
+
// selection start as aria-disabled to keyboard and screen-reader users.
|
|
131
|
+
if (
|
|
132
|
+
minRangeSpan != null &&
|
|
133
|
+
distance > 0 &&
|
|
134
|
+
distance < minRangeSpan - 1
|
|
135
|
+
) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
96
140
|
// Check custom constraints (convert to Date for public API compatibility)
|
|
97
141
|
if (dateConstraints) {
|
|
98
142
|
for (const constraint of dateConstraints) {
|
|
@@ -104,7 +148,14 @@ export function useCalendarConstraints(
|
|
|
104
148
|
|
|
105
149
|
return false;
|
|
106
150
|
},
|
|
107
|
-
[
|
|
151
|
+
[
|
|
152
|
+
minDate,
|
|
153
|
+
maxDate,
|
|
154
|
+
dateConstraints,
|
|
155
|
+
maxRangeSpan,
|
|
156
|
+
minRangeSpan,
|
|
157
|
+
rangeAnchor,
|
|
158
|
+
],
|
|
108
159
|
);
|
|
109
160
|
|
|
110
161
|
return {
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
typeScaleVars,
|
|
44
44
|
typographyVars,
|
|
45
45
|
} from '../theme/tokens.stylex';
|
|
46
|
-
import {mergeProps} from '../utils';
|
|
46
|
+
import {mergeProps, isImeKeyEvent} from '../utils';
|
|
47
47
|
import {useTriggerMenu} from './useTriggerMenu';
|
|
48
48
|
import {useChatComposerTokens, isCustomToken} from './useChatComposerTokens';
|
|
49
49
|
import {ensureCaretInside, insertTextAtCursor} from './chatComposerSelection';
|
|
@@ -559,9 +559,8 @@ export function ChatComposerInput(props: ChatComposerInputProps) {
|
|
|
559
559
|
|
|
560
560
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
561
561
|
// Never submit mid-composition — an IME uses Enter to commit a
|
|
562
|
-
// candidate
|
|
563
|
-
|
|
564
|
-
if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) {
|
|
562
|
+
// candidate. See utils/ime.ts for the full rationale.
|
|
563
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
565
564
|
return;
|
|
566
565
|
}
|
|
567
566
|
|
|
@@ -47,6 +47,7 @@ import {useTooltip} from '../Tooltip';
|
|
|
47
47
|
import {mergeProps, mergeRefs} from '../utils';
|
|
48
48
|
import {indicatorScope} from '../Indicator/indicator.markers.stylex';
|
|
49
49
|
import {useIndicatorFocusRing} from '../hooks/useIndicatorFocusRing';
|
|
50
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
50
51
|
import {useIndicator} from '../Indicator';
|
|
51
52
|
import {themeProps} from '../utils/themeProps';
|
|
52
53
|
import {CheckboxListContext} from '../CheckboxList/CheckboxListContext';
|
|
@@ -301,6 +302,10 @@ export function CheckboxInput({
|
|
|
301
302
|
const id = useId();
|
|
302
303
|
const descriptionID = useId();
|
|
303
304
|
const statusMessageID = useId();
|
|
305
|
+
// Announce the effective required state (form default included) while the
|
|
306
|
+
// native `required` stays bound to the explicit `isRequired` so a layout
|
|
307
|
+
// default never switches on browser validation.
|
|
308
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
304
309
|
|
|
305
310
|
const [, startTransition] = useTransition();
|
|
306
311
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
@@ -421,6 +426,7 @@ export function CheckboxInput({
|
|
|
421
426
|
form={isFocusableDisabled ? '' : undefined}
|
|
422
427
|
readOnly={isReadOnly}
|
|
423
428
|
required={isRequired}
|
|
429
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
424
430
|
onChange={e => {
|
|
425
431
|
if (isDisabled || isBusy || isReadOnly) {
|
|
426
432
|
return;
|
|
@@ -35,6 +35,7 @@ import {useTranslator} from '../i18n';
|
|
|
35
35
|
import {layerAnimations} from '../Layer/layerAnimations.stylex';
|
|
36
36
|
import type {LayerAlignment, LayerPlacement} from '../Layer/useLayer';
|
|
37
37
|
import {usePopover} from '../Popover/usePopover';
|
|
38
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
38
39
|
import {
|
|
39
40
|
colorVars,
|
|
40
41
|
durationVars,
|
|
@@ -46,7 +47,7 @@ import {
|
|
|
46
47
|
typographyVars,
|
|
47
48
|
typeScaleVars,
|
|
48
49
|
} from '../theme/tokens.stylex';
|
|
49
|
-
import {mergeProps} from '../utils';
|
|
50
|
+
import {isRenderable, mergeProps} from '../utils';
|
|
50
51
|
import {composeEventHandlers} from '../utils/composeEventHandlers';
|
|
51
52
|
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
52
53
|
import type {SizeValue} from '../utils/types';
|
|
@@ -344,6 +345,7 @@ export function ComplexSelector<Value>({
|
|
|
344
345
|
...props
|
|
345
346
|
}: ComplexSelectorProps<Value>) {
|
|
346
347
|
const t = useTranslator();
|
|
348
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
347
349
|
const placeholder = placeholderFromProps ?? t('@astryx.selector.placeholder');
|
|
348
350
|
const effectiveStatusVariant =
|
|
349
351
|
variant === 'ghost' && statusVariant === 'attached'
|
|
@@ -439,6 +441,11 @@ export function ComplexSelector<Value>({
|
|
|
439
441
|
|
|
440
442
|
const triggerContent = triggerLabel ?? placeholder;
|
|
441
443
|
|
|
444
|
+
const startIconSlot = renderIconSlot(startIcon, {
|
|
445
|
+
size: 'sm',
|
|
446
|
+
color: 'secondary',
|
|
447
|
+
});
|
|
448
|
+
|
|
442
449
|
const content = (
|
|
443
450
|
<div id={contentId} {...stylex.props(styles.content, contentXstyle)}>
|
|
444
451
|
{children(optimisticValue, commitValue, close, {
|
|
@@ -467,9 +474,12 @@ export function ComplexSelector<Value>({
|
|
|
467
474
|
inputWrapperStyles.base,
|
|
468
475
|
styles.triggerContainer,
|
|
469
476
|
styles[size],
|
|
470
|
-
|
|
477
|
+
// The ring belongs to the wrapper (the focusable `<button>` sits
|
|
478
|
+
// inside it), but it must still be a KEYBOARD ring: `:focus-within`
|
|
479
|
+
// matched a mouse click on the trigger and drew the outline for
|
|
480
|
+
// pointer users too. `focusWithin` here is `:has(:focus-visible)`.
|
|
481
|
+
focusOutlineStyles.focusWithin,
|
|
471
482
|
variant === 'ghost' && styles.triggerGhost,
|
|
472
|
-
variant === 'ghost' && focusOutlineStyles.focusWithin,
|
|
473
483
|
isDisabled && inputWrapperStyles.disabled,
|
|
474
484
|
variant === 'ghost' && isDisabled && styles.triggerGhostDisabled,
|
|
475
485
|
isDisabled && styles.disabled,
|
|
@@ -479,8 +489,7 @@ export function ComplexSelector<Value>({
|
|
|
479
489
|
className,
|
|
480
490
|
style,
|
|
481
491
|
)}>
|
|
482
|
-
{
|
|
483
|
-
renderIconSlot(startIcon, {size: 'sm', color: 'secondary'})}
|
|
492
|
+
{isRenderable(startIconSlot) && startIconSlot}
|
|
484
493
|
<button
|
|
485
494
|
ref={triggerRef}
|
|
486
495
|
id={triggerId}
|
|
@@ -490,7 +499,7 @@ export function ComplexSelector<Value>({
|
|
|
490
499
|
aria-controls={contentId}
|
|
491
500
|
aria-describedby={ariaDescribedBy}
|
|
492
501
|
aria-labelledby={labelId}
|
|
493
|
-
aria-required={
|
|
502
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
494
503
|
aria-invalid={status?.type === 'error' ? 'true' : undefined}
|
|
495
504
|
aria-busy={isBusy || undefined}
|
|
496
505
|
disabled={isDisabled}
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
easeVars,
|
|
64
64
|
shadowVars,
|
|
65
65
|
} from '../theme/tokens.stylex';
|
|
66
|
-
import {mergeProps, mergeRefs} from '../utils';
|
|
66
|
+
import {mergeProps, mergeRefs, isImeKeyEvent} from '../utils';
|
|
67
67
|
import type {BaseProps} from '../BaseProps';
|
|
68
68
|
import type {StyleXStyles} from '../theme/types';
|
|
69
69
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -334,7 +334,9 @@ export function ContextMenu({
|
|
|
334
334
|
if (e.key !== 'Escape') {
|
|
335
335
|
return;
|
|
336
336
|
}
|
|
337
|
-
if (e
|
|
337
|
+
if (isImeKeyEvent(e)) {
|
|
338
|
+
// Ignore Escape that is committing/cancelling an IME composition;
|
|
339
|
+
// see utils/ime.ts for why.
|
|
338
340
|
return;
|
|
339
341
|
}
|
|
340
342
|
e.preventDefault();
|
|
@@ -536,6 +536,27 @@ describe('DateInput', () => {
|
|
|
536
536
|
expect(onChange).toHaveBeenCalledWith('2026-03-15');
|
|
537
537
|
});
|
|
538
538
|
|
|
539
|
+
it('does not commit on a composing Enter (IME)', () => {
|
|
540
|
+
const onChange = vi.fn();
|
|
541
|
+
render(<DateInput label="Date" onChange={onChange} />);
|
|
542
|
+
|
|
543
|
+
const input = screen.getByRole('combobox');
|
|
544
|
+
fireEvent.change(input, {target: {value: '03/15/2026'}});
|
|
545
|
+
onChange.mockClear();
|
|
546
|
+
|
|
547
|
+
// The composing keydown (isComposing / legacy keyCode 229) that commits an
|
|
548
|
+
// IME candidate fires before compositionend; it must not be read as
|
|
549
|
+
// "commit the typed date".
|
|
550
|
+
fireEvent.keyDown(input, {key: 'Enter', isComposing: true});
|
|
551
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
552
|
+
fireEvent.keyDown(input, {key: 'Enter', keyCode: 229});
|
|
553
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
554
|
+
|
|
555
|
+
// A real, non-composing Enter still commits.
|
|
556
|
+
fireEvent.keyDown(input, {key: 'Enter'});
|
|
557
|
+
expect(onChange).toHaveBeenCalledWith('2026-03-15');
|
|
558
|
+
});
|
|
559
|
+
|
|
539
560
|
// --- Arrow-down opens calendar popover ---
|
|
540
561
|
|
|
541
562
|
// Note: Tests involving popover rendering (show/hide with calendar)
|
|
@@ -58,9 +58,10 @@ import {
|
|
|
58
58
|
} from '../Calendar';
|
|
59
59
|
import {useCalendarConstraints} from '../Calendar/hooks';
|
|
60
60
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
61
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
61
62
|
import {usePopover} from '../Popover';
|
|
62
63
|
import {useTooltip} from '../Tooltip';
|
|
63
|
-
import {getInputARIA, parseDateInput} from '../utils';
|
|
64
|
+
import {getInputARIA, isImeKeyEvent, parseDateInput} from '../utils';
|
|
64
65
|
import {
|
|
65
66
|
plainDateFromISO,
|
|
66
67
|
plainDateToISO,
|
|
@@ -399,6 +400,7 @@ export function DateInput({
|
|
|
399
400
|
...rest
|
|
400
401
|
}: DateInputProps) {
|
|
401
402
|
const t = useTranslator();
|
|
403
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
402
404
|
const placeholder =
|
|
403
405
|
placeholderFromProps ?? t('@astryx.dateInput.placeholder');
|
|
404
406
|
const size = useSize(sizeProp, 'md');
|
|
@@ -627,6 +629,14 @@ export function DateInput({
|
|
|
627
629
|
// Handle keyboard events on input
|
|
628
630
|
const handleInputKeyDown = useCallback(
|
|
629
631
|
(e: React.KeyboardEvent) => {
|
|
632
|
+
// An in-progress IME composition uses Enter to commit the candidate and
|
|
633
|
+
// Escape to cancel it; that composing keydown fires before
|
|
634
|
+
// compositionend, so without this guard a Korean/Japanese/Chinese user
|
|
635
|
+
// committing a syllable with Enter would instead commit the pending date
|
|
636
|
+
// (or Escape would close the calendar mid-composition). See utils/ime.ts.
|
|
637
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
630
640
|
if (e.key === 'Escape' && popover.isOpen) {
|
|
631
641
|
e.preventDefault();
|
|
632
642
|
popover.hide();
|
|
@@ -729,7 +739,7 @@ export function DateInput({
|
|
|
729
739
|
readOnly={showsDisabledMessage || undefined}
|
|
730
740
|
aria-labelledby={ariaLabelledBy}
|
|
731
741
|
aria-describedby={ariaDescribedBy}
|
|
732
|
-
aria-required={
|
|
742
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
733
743
|
aria-invalid={
|
|
734
744
|
status?.type === 'error' || !isInputValid ? 'true' : undefined
|
|
735
745
|
}
|
|
@@ -98,6 +98,18 @@ export const docs = {
|
|
|
98
98
|
type: 'Array<(date: Date) => boolean>',
|
|
99
99
|
description: 'Custom constraint functions to disable specific dates.',
|
|
100
100
|
},
|
|
101
|
+
{
|
|
102
|
+
name: 'maxRangeSpan',
|
|
103
|
+
type: 'number',
|
|
104
|
+
description:
|
|
105
|
+
'Maximum days a selected range may span, counting both endpoints (`7` = a 7-day window, start + 6). Once a start is picked, days beyond this distance are disabled so the range cannot stretch past the cap. Rolling window relative to the start — for fixed calendar bounds use `min`/`max`. Constrains selection only; it never rewrites a `value` already wider than the cap (flag that with `status`).',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'minRangeSpan',
|
|
109
|
+
type: 'number',
|
|
110
|
+
description:
|
|
111
|
+
'Minimum days a selected range must span, counting both endpoints (`2` forbids a single-day range). Once a start is picked, days closer than this are disabled. Defaults to 1 (same-day start and end allowed).',
|
|
112
|
+
},
|
|
101
113
|
{
|
|
102
114
|
name: 'presets',
|
|
103
115
|
type: 'Array<DateRangePreset>',
|
|
@@ -313,6 +325,10 @@ export const docsDense = {
|
|
|
313
325
|
min: 'min selectable date: ISODateString template literal type (YYYY-MM-DD); use string literal or cast `as ISODateString`',
|
|
314
326
|
max: 'max selectable date: ISODateString template literal type (YYYY-MM-DD); use string literal or cast `as ISODateString`',
|
|
315
327
|
dateConstraints: 'custom constraint fns to disable dates',
|
|
328
|
+
maxRangeSpan:
|
|
329
|
+
'max days a range may span, both endpoints counted (7 = a 7-day window); caps the window from the picked start. Selection-only; does not rewrite an over-wide value',
|
|
330
|
+
minRangeSpan:
|
|
331
|
+
'min days a range must span, both endpoints counted (2 forbids a single-day range); default 1',
|
|
316
332
|
presets: 'preset ranges as quick-select options',
|
|
317
333
|
hasClear: 'clear button when range is set (default true)',
|
|
318
334
|
placeholder: 'placeholder when empty',
|