@xh/hoist 86.4.0 → 86.4.2
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 +26 -0
- package/build/types/kit/react-day-picker/index.d.ts +5 -0
- package/build/types/kit/swiper/index.d.ts +3 -4
- package/build/types/mobile/cmp/input/DateInput.d.ts +16 -17
- package/kit/golden-layout/impl/js/LayoutManager.js +6 -6
- package/kit/golden-layout/impl/js/controls/DragProxy.js +2 -2
- package/kit/golden-layout/impl/js/items/Root.js +2 -2
- package/kit/golden-layout/impl/js/utils/utils.js +14 -0
- package/kit/react-day-picker/index.ts +17 -0
- package/mobile/cmp/input/DateInput.scss +38 -28
- package/mobile/cmp/input/DateInput.ts +91 -83
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 86.4.2 - 2026-08-20
|
|
4
|
+
|
|
5
|
+
### 🎁 New Features
|
|
6
|
+
|
|
7
|
+
* Mobile `DateInput` now opens a themeable calendar picker (react-day-picker) within a dialog,
|
|
8
|
+
replacing the OS-provided picker of the native `<input type="date">` element. Adds a
|
|
9
|
+
`formatString` prop to control the in-input display of the value, and a `dayPickerProps` escape
|
|
10
|
+
hatch to pass props (e.g. `disabled` day matchers) directly to the underlying calendar.
|
|
11
|
+
|
|
12
|
+
### 📚 Libraries
|
|
13
|
+
|
|
14
|
+
* react-day-picker `added @ 9.14`
|
|
15
|
+
|
|
16
|
+
## 86.4.1 - 2026-08-11
|
|
17
|
+
|
|
18
|
+
### 🐞 Bug Fixes
|
|
19
|
+
|
|
20
|
+
* Fixed `DashContainer` sizing regression introduced with the golden-layout fork in v86.0.0. CSS
|
|
21
|
+
padding applied to the container element was incorrectly counted as available layout space,
|
|
22
|
+
causing dashboard content to render oversized and cut off.
|
|
23
|
+
* Revert swiper library to previous version to address swiping regressions (#4559)
|
|
24
|
+
|
|
25
|
+
### 📚 Libraries
|
|
26
|
+
* swiper `14.0 -> 12.1`
|
|
27
|
+
|
|
28
|
+
|
|
3
29
|
## 86.4.0 - 2026-07-15
|
|
4
30
|
|
|
5
31
|
### 🎁 New Features
|
|
@@ -28,7 +28,6 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
28
28
|
onScrollbarDragMove?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
29
29
|
onScrollbarDragEnd?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
30
30
|
onZoomChange?: (swiper: import("swiper/types").Swiper, scale: number, imageEl: HTMLElement, slideEl: HTMLElement) => void;
|
|
31
|
-
onVirtualUpdate?: (swiper: import("swiper/types").Swiper) => void;
|
|
32
31
|
onInit?: (swiper: import("swiper/types").Swiper) => any;
|
|
33
32
|
onBeforeDestroy?: (swiper: import("swiper/types").Swiper) => void;
|
|
34
33
|
onSlidesUpdated?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -54,8 +53,8 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
54
53
|
onReachEnd?: (swiper: import("swiper/types").Swiper) => void;
|
|
55
54
|
onToEdge?: (swiper: import("swiper/types").Swiper) => void;
|
|
56
55
|
onFromEdge?: (swiper: import("swiper/types").Swiper) => void;
|
|
57
|
-
onSetTranslate?: (swiper: import("swiper/types").Swiper, translate: number
|
|
58
|
-
onSetTransition?: (swiper: import("swiper/types").Swiper, transition: number
|
|
56
|
+
onSetTranslate?: (swiper: import("swiper/types").Swiper, translate: number) => void;
|
|
57
|
+
onSetTransition?: (swiper: import("swiper/types").Swiper, transition: number) => void;
|
|
59
58
|
onResize?: (swiper: import("swiper/types").Swiper) => void;
|
|
60
59
|
onObserverUpdate?: (swiper: import("swiper/types").Swiper) => void;
|
|
61
60
|
onBeforeLoopFix?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -78,7 +77,7 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
78
77
|
onBeforeInit?: (swiper: import("swiper/types").Swiper) => void;
|
|
79
78
|
onBeforeResize?: (swiper: import("swiper/types").Swiper) => void;
|
|
80
79
|
onBeforeSlideChangeStart?: (swiper: import("swiper/types").Swiper) => void;
|
|
81
|
-
onBeforeTransitionStart?: (swiper: import("swiper/types").Swiper, speed: number, internal:
|
|
80
|
+
onBeforeTransitionStart?: (swiper: import("swiper/types").Swiper, speed: number, internal: any) => void;
|
|
82
81
|
onChangeDirection?: (swiper: import("swiper/types").Swiper) => void;
|
|
83
82
|
onDoubleClick?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
84
83
|
onDestroy?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HoistInputProps } from '@xh/hoist/cmp/input';
|
|
2
2
|
import { HoistProps, LayoutProps, StyleProps } from '@xh/hoist/core';
|
|
3
|
+
import { DayPickerProps } from '@xh/hoist/kit/react-day-picker';
|
|
3
4
|
import '@xh/hoist/mobile/register';
|
|
4
5
|
import { LocalDate } from '@xh/hoist/utils/datetime';
|
|
5
6
|
import type { Property } from 'csstype';
|
|
@@ -7,26 +8,28 @@ import { ReactElement } from 'react';
|
|
|
7
8
|
import './DateInput.scss';
|
|
8
9
|
export interface DateInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps {
|
|
9
10
|
value?: Date | LocalDate;
|
|
11
|
+
/**
|
|
12
|
+
* Escape hatch to pass props directly to the underlying react-day-picker calendar.
|
|
13
|
+
* `disabled` matchers are combined with those generated by minDate/maxDate. Selection
|
|
14
|
+
* wiring (`mode`, `selected`, `onSelect`) cannot be overridden.
|
|
15
|
+
*/
|
|
16
|
+
dayPickerProps?: Partial<DayPickerProps>;
|
|
10
17
|
/** True to show a "clear" button aligned to the right of the control. Defaults to false. */
|
|
11
18
|
enableClear?: boolean;
|
|
19
|
+
/** MomentJS format string for the in-input display of the value. Defaults to `YYYY-MM-DD`. */
|
|
20
|
+
formatString?: string;
|
|
12
21
|
/** Icon to display inline on the left side of the input. */
|
|
13
22
|
leftIcon?: ReactElement;
|
|
14
23
|
/** Icon to display inline on the right side of the input. Defaults to a calendar icon. */
|
|
15
24
|
rightIcon?: ReactElement;
|
|
16
25
|
/**
|
|
17
|
-
* Maximum (inclusive) valid date.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Note this is distinct in these ways from FormModel based validation, which will leave an
|
|
21
|
-
* invalid date entry in place but flag as invalid via FormField. For cases where it is
|
|
22
|
-
* possible to use FormField, that is often a better choice.
|
|
26
|
+
* Maximum (inclusive) valid date. Days beyond this bound are disabled within the calendar,
|
|
27
|
+
* and the calendar cannot be navigated past its month.
|
|
23
28
|
*/
|
|
24
29
|
maxDate?: Date | LocalDate;
|
|
25
30
|
/**
|
|
26
|
-
* Minimum (inclusive) valid date.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* See note re. validation on maxDate, above.
|
|
31
|
+
* Minimum (inclusive) valid date. Days before this bound are disabled within the calendar,
|
|
32
|
+
* and the calendar cannot be navigated before its month.
|
|
30
33
|
*/
|
|
31
34
|
minDate?: Date | LocalDate;
|
|
32
35
|
/** Alignment of entry text within control, default 'left'. */
|
|
@@ -35,12 +38,8 @@ export interface DateInputProps extends HoistProps, HoistInputProps, StyleProps,
|
|
|
35
38
|
valueType?: 'date' | 'localDate';
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
38
|
-
* A mobile-first calendar control for choosing a Date
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* browsers.
|
|
42
|
-
*
|
|
43
|
-
* The in-input display and picker UI follow the user's OS locale. Values are read from and
|
|
44
|
-
* written to the underlying input as ISO-8601 (`YYYY-MM-DD`) strings.
|
|
41
|
+
* A mobile-first calendar control for choosing a Date. Tapping the input opens a calendar
|
|
42
|
+
* picker (react-day-picker) hosted within a modal dialog. Selecting a day commits the value
|
|
43
|
+
* and dismisses the picker.
|
|
45
44
|
*/
|
|
46
45
|
export declare const DateInput: import("react").FC<DateInputProps>, dateInput: import("@xh/hoist/core").ElementFactory<DateInputProps>;
|
|
@@ -210,8 +210,8 @@ lm.utils.copy( lm.LayoutManager.prototype, {
|
|
|
210
210
|
this.width = width;
|
|
211
211
|
this.height = height;
|
|
212
212
|
} else {
|
|
213
|
-
this.width = containerNode
|
|
214
|
-
this.height = containerNode
|
|
213
|
+
this.width = lm.utils.contentWidth( containerNode );
|
|
214
|
+
this.height = lm.utils.contentHeight( containerNode );
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
if( this.isInitialised === true ) {
|
|
@@ -219,8 +219,8 @@ lm.utils.copy( lm.LayoutManager.prototype, {
|
|
|
219
219
|
|
|
220
220
|
if( this._maximisedItem ) {
|
|
221
221
|
var maxNode = this._maximisedItem.element;
|
|
222
|
-
maxNode.style.width =
|
|
223
|
-
maxNode.style.height =
|
|
222
|
+
maxNode.style.width = lm.utils.contentWidth( containerNode ) + 'px';
|
|
223
|
+
maxNode.style.height = lm.utils.contentHeight( containerNode ) + 'px';
|
|
224
224
|
this._maximisedItem.callDownwards( 'setSize' );
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -379,8 +379,8 @@ lm.utils.copy( lm.LayoutManager.prototype, {
|
|
|
379
379
|
itemNode.classList.add( 'lm_maximised' );
|
|
380
380
|
itemNode.after( placeholderNode );
|
|
381
381
|
rootNode.prepend( itemNode );
|
|
382
|
-
itemNode.style.width =
|
|
383
|
-
itemNode.style.height =
|
|
382
|
+
itemNode.style.width = lm.utils.contentWidth( containerNode ) + 'px';
|
|
383
|
+
itemNode.style.height = lm.utils.contentHeight( containerNode ) + 'px';
|
|
384
384
|
contentItem.callDownwards( 'setSize' );
|
|
385
385
|
this._maximisedItem.emit( 'maximised' );
|
|
386
386
|
this.emit( 'stateChanged' );
|
|
@@ -62,8 +62,8 @@ lm.controls.DragProxy = function( x, y, dragListener, layoutManager, contentItem
|
|
|
62
62
|
|
|
63
63
|
this._minX = containerRect.left + window.scrollX;
|
|
64
64
|
this._minY = containerRect.top + window.scrollY;
|
|
65
|
-
this._maxX = containerNode
|
|
66
|
-
this._maxY = containerNode
|
|
65
|
+
this._maxX = lm.utils.contentWidth(containerNode) + this._minX;
|
|
66
|
+
this._maxY = lm.utils.contentHeight(containerNode) + this._minY;
|
|
67
67
|
this._width = this.element.clientWidth;
|
|
68
68
|
this._height = this.element.clientHeight;
|
|
69
69
|
|
|
@@ -29,8 +29,8 @@ lm.utils.copy( lm.items.Root.prototype, {
|
|
|
29
29
|
|
|
30
30
|
setSize: function( width, height ) {
|
|
31
31
|
var containerNode = this._containerElement;
|
|
32
|
-
width = ( typeof width === 'undefined' ) ?
|
|
33
|
-
height = ( typeof height === 'undefined' ) ?
|
|
32
|
+
width = ( typeof width === 'undefined' ) ? lm.utils.contentWidth( containerNode ) : width;
|
|
33
|
+
height = ( typeof height === 'undefined' ) ? lm.utils.contentHeight( containerNode ) : height;
|
|
34
34
|
|
|
35
35
|
this.element.style.width = width + 'px';
|
|
36
36
|
this.element.style.height = height + 'px';
|
|
@@ -159,6 +159,20 @@ lm.utils.getUniqueId = function() {
|
|
|
159
159
|
.replace( '.', '' );
|
|
160
160
|
};
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Content-box dimensions - equivalent to jQuery's .width()/.height(),
|
|
164
|
+
* which the pre-fork code used. clientWidth/Height include CSS padding.
|
|
165
|
+
*/
|
|
166
|
+
lm.utils.contentWidth = function( node ) {
|
|
167
|
+
var style = getComputedStyle( node );
|
|
168
|
+
return node.clientWidth - parseFloat( style.paddingLeft ) - parseFloat( style.paddingRight );
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
lm.utils.contentHeight = function( node ) {
|
|
172
|
+
var style = getComputedStyle( node );
|
|
173
|
+
return node.clientHeight - parseFloat( style.paddingTop ) - parseFloat( style.paddingBottom );
|
|
174
|
+
};
|
|
175
|
+
|
|
162
176
|
/**
|
|
163
177
|
* A basic XSS filter. It is ultimately up to the
|
|
164
178
|
* implementing developer to make sure their particular
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {elementFactory} from '@xh/hoist/core';
|
|
8
|
+
import {ComponentType} from 'react';
|
|
9
|
+
import {DayPicker} from 'react-day-picker';
|
|
10
|
+
import 'react-day-picker/style.css';
|
|
11
|
+
|
|
12
|
+
export {DayPicker};
|
|
13
|
+
export type {DayPickerProps, Matcher} from 'react-day-picker';
|
|
14
|
+
|
|
15
|
+
// DayPicker's props are a discriminated union over its selection modes. Cast to ComponentType to
|
|
16
|
+
// keep our element factory ergonomic and produce portable type declarations.
|
|
17
|
+
export const dayPicker = elementFactory(DayPicker as ComponentType<any>);
|
|
@@ -7,41 +7,22 @@
|
|
|
7
7
|
flex: none;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// Read-only display of the formatted value - tap to open the calendar picker.
|
|
11
|
+
&__display {
|
|
12
|
+
flex: 1;
|
|
13
|
+
min-width: 0;
|
|
13
14
|
height: var(--xh-mobile-input-height-px);
|
|
14
|
-
padding: 0 var(--xh-pad-px);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Strip native date chrome - Icon.calendar() is the sole affordance.
|
|
18
|
-
.text-input {
|
|
19
|
-
appearance: none;
|
|
20
|
-
-webkit-appearance: none;
|
|
21
|
-
|
|
22
|
-
// Center the value vertically within the fixed-height control. On iOS the formatted value sits
|
|
23
|
-
// in ::-webkit-date-and-time-value, which carries a default margin and aligns to the top of the
|
|
24
|
-
// content box - line-height centers it (single line, no vertical padding) without disturbing
|
|
25
|
-
// the horizontal text-align driven by the textAlign prop.
|
|
26
15
|
line-height: var(--xh-mobile-input-height-px);
|
|
16
|
+
padding: 0 var(--xh-pad-px);
|
|
17
|
+
white-space: nowrap;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
text-overflow: ellipsis;
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
&::-webkit-date-and-time-value {
|
|
30
|
-
text-align: inherit;
|
|
31
|
-
margin: 0;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
&:disabled {
|
|
21
|
+
&--disabled {
|
|
35
22
|
color: var(--xh-text-color-muted);
|
|
36
|
-
opacity: 1;
|
|
37
23
|
}
|
|
38
24
|
}
|
|
39
25
|
|
|
40
|
-
&__input {
|
|
41
|
-
flex: 1;
|
|
42
|
-
min-width: 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
26
|
&__picker-button {
|
|
46
27
|
flex: none;
|
|
47
28
|
margin: 0 6px;
|
|
@@ -55,4 +36,33 @@
|
|
|
55
36
|
padding-left: 0;
|
|
56
37
|
color: var(--xh-text-color-muted) !important;
|
|
57
38
|
}
|
|
39
|
+
|
|
40
|
+
&__picker-dialog {
|
|
41
|
+
// No title provided - suppress the empty title bar rendered by Dialog.
|
|
42
|
+
.xh-dialog__title:empty {
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Theme react-day-picker to match the app via its published CSS custom props.
|
|
47
|
+
.rdp-root {
|
|
48
|
+
--rdp-accent-color: var(--xh-accent-color);
|
|
49
|
+
--rdp-accent-background-color: var(--xh-bg-alt);
|
|
50
|
+
--rdp-today-color: var(--xh-accent-color);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// blueprint-datetime.css - currently pulled into all bundles via kit/blueprint - embeds
|
|
54
|
+
// react-day-picker v8's stylesheet, whose class names partially collide with the v9 markup
|
|
55
|
+
// rendered here. Reset the clashing v8 rules, most critically the day cells, which v8 styles
|
|
56
|
+
// as flex-box buttons but v9 renders as table cells.
|
|
57
|
+
.rdp-day,
|
|
58
|
+
.rdp-weeknumber {
|
|
59
|
+
display: table-cell;
|
|
60
|
+
width: var(--rdp-day-width);
|
|
61
|
+
max-width: none;
|
|
62
|
+
height: var(--rdp-day-height);
|
|
63
|
+
overflow: visible;
|
|
64
|
+
border: none;
|
|
65
|
+
border-radius: 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
58
68
|
}
|
|
@@ -5,25 +5,40 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
|
|
8
|
-
import {box, hbox} from '@xh/hoist/cmp/layout';
|
|
8
|
+
import {box, div, hbox} from '@xh/hoist/cmp/layout';
|
|
9
9
|
import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core';
|
|
10
|
+
import {fmtDate} from '@xh/hoist/format';
|
|
10
11
|
import {Icon} from '@xh/hoist/icon';
|
|
11
|
-
import {
|
|
12
|
+
import {dayPicker, DayPickerProps} from '@xh/hoist/kit/react-day-picker';
|
|
12
13
|
import {button} from '@xh/hoist/mobile/cmp/button';
|
|
14
|
+
import {dialog} from '@xh/hoist/mobile/cmp/dialog';
|
|
13
15
|
import '@xh/hoist/mobile/register';
|
|
16
|
+
import {bindable, makeObservable} from '@xh/hoist/mobx';
|
|
14
17
|
import {isLocalDate, LocalDate} from '@xh/hoist/utils/datetime';
|
|
15
18
|
import {getTestId, TEST_ID, withDefault} from '@xh/hoist/utils/js';
|
|
16
19
|
import {getLayoutProps} from '@xh/hoist/utils/react';
|
|
20
|
+
import classNames from 'classnames';
|
|
17
21
|
import type {Property} from 'csstype';
|
|
18
|
-
import {
|
|
22
|
+
import {castArray, isNil, omitBy} from 'lodash';
|
|
23
|
+
import {ReactElement} from 'react';
|
|
19
24
|
import './DateInput.scss';
|
|
20
25
|
|
|
21
26
|
export interface DateInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps {
|
|
22
27
|
value?: Date | LocalDate;
|
|
23
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Escape hatch to pass props directly to the underlying react-day-picker calendar.
|
|
31
|
+
* `disabled` matchers are combined with those generated by minDate/maxDate. Selection
|
|
32
|
+
* wiring (`mode`, `selected`, `onSelect`) cannot be overridden.
|
|
33
|
+
*/
|
|
34
|
+
dayPickerProps?: Partial<DayPickerProps>;
|
|
35
|
+
|
|
24
36
|
/** True to show a "clear" button aligned to the right of the control. Defaults to false. */
|
|
25
37
|
enableClear?: boolean;
|
|
26
38
|
|
|
39
|
+
/** MomentJS format string for the in-input display of the value. Defaults to `YYYY-MM-DD`. */
|
|
40
|
+
formatString?: string;
|
|
41
|
+
|
|
27
42
|
/** Icon to display inline on the left side of the input. */
|
|
28
43
|
leftIcon?: ReactElement;
|
|
29
44
|
|
|
@@ -31,20 +46,14 @@ export interface DateInputProps extends HoistProps, HoistInputProps, StyleProps,
|
|
|
31
46
|
rightIcon?: ReactElement;
|
|
32
47
|
|
|
33
48
|
/**
|
|
34
|
-
* Maximum (inclusive) valid date.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* Note this is distinct in these ways from FormModel based validation, which will leave an
|
|
38
|
-
* invalid date entry in place but flag as invalid via FormField. For cases where it is
|
|
39
|
-
* possible to use FormField, that is often a better choice.
|
|
49
|
+
* Maximum (inclusive) valid date. Days beyond this bound are disabled within the calendar,
|
|
50
|
+
* and the calendar cannot be navigated past its month.
|
|
40
51
|
*/
|
|
41
52
|
maxDate?: Date | LocalDate;
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
|
-
* Minimum (inclusive) valid date.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* See note re. validation on maxDate, above.
|
|
55
|
+
* Minimum (inclusive) valid date. Days before this bound are disabled within the calendar,
|
|
56
|
+
* and the calendar cannot be navigated before its month.
|
|
48
57
|
*/
|
|
49
58
|
minDate?: Date | LocalDate;
|
|
50
59
|
|
|
@@ -56,13 +65,9 @@ export interface DateInputProps extends HoistProps, HoistInputProps, StyleProps,
|
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
|
-
* A mobile-first calendar control for choosing a Date
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* browsers.
|
|
63
|
-
*
|
|
64
|
-
* The in-input display and picker UI follow the user's OS locale. Values are read from and
|
|
65
|
-
* written to the underlying input as ISO-8601 (`YYYY-MM-DD`) strings.
|
|
68
|
+
* A mobile-first calendar control for choosing a Date. Tapping the input opens a calendar
|
|
69
|
+
* picker (react-day-picker) hosted within a modal dialog. Selecting a day commits the value
|
|
70
|
+
* and dismisses the picker.
|
|
66
71
|
*/
|
|
67
72
|
export const [DateInput, dateInput] = hoistCmp.withFactory<DateInputProps>({
|
|
68
73
|
displayName: 'DateInput',
|
|
@@ -80,6 +85,13 @@ export const [DateInput, dateInput] = hoistCmp.withFactory<DateInputProps>({
|
|
|
80
85
|
class DateInputModel extends HoistInputModel {
|
|
81
86
|
override xhImpl = true;
|
|
82
87
|
|
|
88
|
+
@bindable pickerIsOpen: boolean = false;
|
|
89
|
+
|
|
90
|
+
constructor() {
|
|
91
|
+
super();
|
|
92
|
+
makeObservable(this);
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
get valueType(): 'date' | 'localDate' {
|
|
84
96
|
return withDefault(this.componentProps.valueType, 'date');
|
|
85
97
|
}
|
|
@@ -97,6 +109,15 @@ class DateInputModel extends HoistInputModel {
|
|
|
97
109
|
return !!enableClear && !disabled && this.renderValue != null;
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
get formatString(): string {
|
|
113
|
+
return withDefault(this.componentProps.formatString, 'YYYY-MM-DD');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get formattedRenderValue(): string {
|
|
117
|
+
const {renderValue} = this;
|
|
118
|
+
return renderValue ? (fmtDate(renderValue, this.formatString) as string) : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
override toExternal(internal: Date | null): Date | LocalDate | null {
|
|
101
122
|
if (this.valueType === 'localDate') return internal ? LocalDate.from(internal) : null;
|
|
102
123
|
return internal;
|
|
@@ -107,43 +128,19 @@ class DateInputModel extends HoistInputModel {
|
|
|
107
128
|
return isLocalDate(external) ? external.date : (external as Date);
|
|
108
129
|
}
|
|
109
130
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!str) {
|
|
113
|
-
this.noteValueChange(null);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
let date = isoToDate(str);
|
|
118
|
-
if (date && this.isOutsideRange(date)) {
|
|
119
|
-
this.logDebug('Value exceeded max/minDate bounds on change - reset to null.');
|
|
120
|
-
date = null;
|
|
121
|
-
// Force the native input back in sync with the reset value. When the prior value was
|
|
122
|
-
// also null, the internalValue observable does not change, so no re-render fires and
|
|
123
|
-
// the out-of-bounds entry would otherwise remain displayed in the control.
|
|
124
|
-
ev.target.value = '';
|
|
125
|
-
}
|
|
126
|
-
this.noteValueChange(date);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
isOutsideRange(date: Date): boolean {
|
|
130
|
-
const {minDate, maxDate} = this,
|
|
131
|
-
stamped = stripTime(date);
|
|
132
|
-
if (minDate && stamped < minDate) return true;
|
|
133
|
-
if (maxDate && stamped > maxDate) return true;
|
|
134
|
-
return false;
|
|
131
|
+
openPicker() {
|
|
132
|
+
if (!this.componentProps.disabled) this.pickerIsOpen = true;
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
135
|
+
onDaySelect = (date: Date) => {
|
|
136
|
+
this.noteValueChange(date ?? null);
|
|
137
|
+
this.doCommit();
|
|
138
|
+
this.pickerIsOpen = false;
|
|
139
|
+
};
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
const cmp = hoistCmp.factory<DateInputModel>(({model, className, ...props}, ref) => {
|
|
145
143
|
const {width, ...layoutProps} = getLayoutProps(props),
|
|
146
|
-
{renderValue} = model,
|
|
147
144
|
textAlign = withDefault(props.textAlign, 'left'),
|
|
148
145
|
leftIcon = withDefault(props.leftIcon, null),
|
|
149
146
|
rightIcon = withDefault(props.rightIcon, Icon.calendar());
|
|
@@ -158,32 +155,60 @@ const cmp = hoistCmp.factory<DateInputModel>(({model, className, ...props}, ref)
|
|
|
158
155
|
},
|
|
159
156
|
items: [
|
|
160
157
|
leftIcon,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
max: dateToIso(model.maxDate),
|
|
167
|
-
disabled: props.disabled,
|
|
168
|
-
tabIndex: props.tabIndex,
|
|
158
|
+
div({
|
|
159
|
+
className: classNames(
|
|
160
|
+
'xh-date-input__display',
|
|
161
|
+
props.disabled ? 'xh-date-input__display--disabled' : null
|
|
162
|
+
),
|
|
169
163
|
style: {textAlign},
|
|
164
|
+
tabIndex: props.tabIndex,
|
|
170
165
|
[TEST_ID]: props.testId,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
onFocus: model.onFocus,
|
|
174
|
-
onBlur: model.onBlur
|
|
166
|
+
item: model.formattedRenderValue,
|
|
167
|
+
onClick: () => model.openPicker()
|
|
175
168
|
}),
|
|
176
169
|
clearButton(),
|
|
177
170
|
rightIcon &&
|
|
178
171
|
box({
|
|
179
172
|
className: 'xh-date-input__picker-button',
|
|
180
|
-
onClick: () => model.
|
|
173
|
+
onClick: () => model.openPicker(),
|
|
181
174
|
item: rightIcon
|
|
182
|
-
})
|
|
175
|
+
}),
|
|
176
|
+
pickerDialog()
|
|
183
177
|
]
|
|
184
178
|
});
|
|
185
179
|
});
|
|
186
180
|
|
|
181
|
+
const pickerDialog = hoistCmp.factory<DateInputModel>(({model}) => {
|
|
182
|
+
let {minDate, maxDate, renderValue} = model,
|
|
183
|
+
{dayPickerProps} = model.componentProps,
|
|
184
|
+
disabledDays = [];
|
|
185
|
+
if (minDate) disabledDays.push({before: minDate});
|
|
186
|
+
if (maxDate) disabledDays.push({after: maxDate});
|
|
187
|
+
if (dayPickerProps?.disabled) disabledDays.push(...castArray(dayPickerProps.disabled));
|
|
188
|
+
|
|
189
|
+
dayPickerProps = omitBy(
|
|
190
|
+
{
|
|
191
|
+
defaultMonth: renderValue,
|
|
192
|
+
startMonth: minDate,
|
|
193
|
+
endMonth: maxDate,
|
|
194
|
+
...dayPickerProps,
|
|
195
|
+
mode: 'single',
|
|
196
|
+
required: true,
|
|
197
|
+
selected: renderValue,
|
|
198
|
+
disabled: disabledDays,
|
|
199
|
+
onSelect: model.onDaySelect
|
|
200
|
+
},
|
|
201
|
+
isNil
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return dialog({
|
|
205
|
+
isOpen: model.pickerIsOpen,
|
|
206
|
+
className: 'xh-date-input__picker-dialog',
|
|
207
|
+
onCancel: () => (model.pickerIsOpen = false),
|
|
208
|
+
content: dayPicker(dayPickerProps)
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
187
212
|
const clearButton = hoistCmp.factory<DateInputModel>(({model}) =>
|
|
188
213
|
button({
|
|
189
214
|
className: 'xh-date-input__clear-button',
|
|
@@ -193,8 +218,6 @@ const clearButton = hoistCmp.factory<DateInputModel>(({model}) =>
|
|
|
193
218
|
omit: !model.showClearButton,
|
|
194
219
|
testId: getTestId(model.componentProps, 'clear-btn'),
|
|
195
220
|
onClick: () => {
|
|
196
|
-
// Intentionally no refocus after clearing - on iOS/Android, focusing the native
|
|
197
|
-
// date input reopens the OS picker, which is unwanted UX after an explicit clear.
|
|
198
221
|
model.noteValueChange(null);
|
|
199
222
|
model.doCommit();
|
|
200
223
|
}
|
|
@@ -202,28 +225,13 @@ const clearButton = hoistCmp.factory<DateInputModel>(({model}) =>
|
|
|
202
225
|
);
|
|
203
226
|
|
|
204
227
|
//---------------------------------
|
|
205
|
-
// Local helpers
|
|
228
|
+
// Local helpers
|
|
206
229
|
//---------------------------------
|
|
207
230
|
function resolveBoundDate(val: Date | LocalDate | undefined | null): Date | null {
|
|
208
231
|
if (val == null) return null;
|
|
209
232
|
return isLocalDate(val) ? val.date : stripTime(val);
|
|
210
233
|
}
|
|
211
234
|
|
|
212
|
-
function isoToDate(iso: string): Date | null {
|
|
213
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
214
|
-
if (!match) return null;
|
|
215
|
-
const [, y, m, d] = match;
|
|
216
|
-
return new Date(Number(y), Number(m) - 1, Number(d));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function dateToIso(date: Date | null | undefined): string | undefined {
|
|
220
|
-
if (date == null) return undefined;
|
|
221
|
-
const y = date.getFullYear(),
|
|
222
|
-
m = String(date.getMonth() + 1).padStart(2, '0'),
|
|
223
|
-
d = String(date.getDate()).padStart(2, '0');
|
|
224
|
-
return `${y}-${m}-${d}`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
235
|
function stripTime(date: Date): Date {
|
|
228
236
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
229
237
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "86.4.
|
|
3
|
+
"version": "86.4.2",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"numbro": "~2.5.0",
|
|
76
76
|
"onsenui": "~2.12.8",
|
|
77
77
|
"qs": "~6.15.0",
|
|
78
|
+
"react-day-picker": "^9.14.0",
|
|
78
79
|
"react-dropzone": "~15.0.0",
|
|
79
80
|
"react-grid-layout": "~2.2.3",
|
|
80
81
|
"react-markdown": "~10.1.0",
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
"semver": "~7.8.0",
|
|
91
92
|
"short-unique-id": "~5.3.2",
|
|
92
93
|
"store2": "~2.14.3",
|
|
93
|
-
"swiper": "^
|
|
94
|
+
"swiper": "^12.1.2",
|
|
94
95
|
"ts-morph": "^27.0.2",
|
|
95
96
|
"tsx": "^4.21.0",
|
|
96
97
|
"ua-parser-js": "~2.0.4",
|