@xh/hoist 87.1.0 → 87.2.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/.npmignore +39 -0
- package/CHANGELOG.md +99 -20
- package/admin/AdminUtils.ts +64 -0
- package/admin/App.scss +88 -0
- package/admin/columns/UserData.ts +30 -3
- package/admin/detail/RestDetailModel.ts +64 -0
- package/admin/detail/RestDetailPanel.ts +106 -0
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +27 -94
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +23 -60
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +1 -2
- package/admin/tabs/general/alertBanner/AlertBannerPanel.ts +2 -13
- package/admin/tabs/general/config/ConfigDetailPanel.ts +72 -0
- package/admin/tabs/general/config/ConfigPanel.ts +41 -53
- package/admin/tabs/general/config/ConfigPanelModel.ts +3 -0
- package/admin/tabs/general/config/ConfigValue.scss +14 -0
- package/admin/tabs/general/config/ConfigValue.ts +104 -65
- package/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.ts +93 -0
- package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +4 -0
- package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +2 -0
- package/admin/tabs/userData/prefs/UserPreferenceDetailPanel.ts +62 -0
- package/admin/tabs/userData/prefs/UserPreferenceModel.ts +10 -3
- package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -1
- package/admin/tabs/userData/roles/details/RoleDetails.scss +2 -19
- package/admin/tabs/userData/roles/details/RoleDetails.ts +13 -13
- package/appcontainer/AboutDialogModel.ts +1 -0
- package/appcontainer/AppContainerModel.ts +3 -0
- package/appcontainer/AppStateModel.ts +1 -0
- package/appcontainer/BannerSourceModel.ts +1 -0
- package/appcontainer/ChangelogDialogModel.ts +1 -0
- package/appcontainer/ColChooserOptionsModel.ts +1 -0
- package/appcontainer/ExceptionDialogModel.ts +1 -0
- package/appcontainer/FeedbackDialogModel.ts +1 -0
- package/appcontainer/ImpersonationBarModel.ts +1 -0
- package/appcontainer/MessageSourceModel.ts +1 -0
- package/appcontainer/OptionsDialogModel.ts +1 -0
- package/appcontainer/PageStateModel.ts +1 -0
- package/appcontainer/RouterModel.ts +2 -0
- package/appcontainer/SizingModeModel.ts +1 -0
- package/appcontainer/ThemeModel.ts +1 -0
- package/appcontainer/ToastSourceModel.ts +1 -0
- package/appcontainer/UserAgentModel.ts +1 -0
- package/appcontainer/ViewportSizeModel.ts +1 -0
- package/build/types/admin/AdminUtils.d.ts +29 -0
- package/build/types/admin/columns/UserData.d.ts +3 -0
- package/build/types/admin/detail/RestDetailModel.d.ts +20 -0
- package/build/types/admin/detail/RestDetailPanel.d.ts +34 -0
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +5 -17
- package/build/types/admin/tabs/general/config/ConfigDetailPanel.d.ts +7 -0
- package/build/types/admin/tabs/general/config/ConfigValue.d.ts +10 -4
- package/build/types/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.d.ts +3 -0
- package/build/types/admin/tabs/userData/prefs/UserPreferenceDetailPanel.d.ts +3 -0
- package/build/types/admin/tabs/userData/prefs/UserPreferenceModel.d.ts +3 -0
- package/build/types/appcontainer/AboutDialogModel.d.ts +1 -0
- package/build/types/appcontainer/AppContainerModel.d.ts +1 -0
- package/build/types/appcontainer/AppStateModel.d.ts +1 -0
- package/build/types/appcontainer/BannerSourceModel.d.ts +1 -0
- package/build/types/appcontainer/ChangelogDialogModel.d.ts +1 -0
- package/build/types/appcontainer/ColChooserOptionsModel.d.ts +1 -0
- package/build/types/appcontainer/ExceptionDialogModel.d.ts +1 -0
- package/build/types/appcontainer/FeedbackDialogModel.d.ts +1 -0
- package/build/types/appcontainer/ImpersonationBarModel.d.ts +1 -0
- package/build/types/appcontainer/MessageSourceModel.d.ts +1 -0
- package/build/types/appcontainer/OptionsDialogModel.d.ts +1 -0
- package/build/types/appcontainer/PageStateModel.d.ts +1 -0
- package/build/types/appcontainer/RouterModel.d.ts +1 -0
- package/build/types/appcontainer/SizingModeModel.d.ts +1 -0
- package/build/types/appcontainer/ThemeModel.d.ts +1 -0
- package/build/types/appcontainer/ToastSourceModel.d.ts +1 -0
- package/build/types/appcontainer/UserAgentModel.d.ts +1 -0
- package/build/types/appcontainer/ViewportSizeModel.d.ts +1 -0
- package/build/types/cmp/ag-grid/AgGridModel.d.ts +3 -1
- package/build/types/cmp/card/Card.d.ts +2 -2
- package/build/types/cmp/chart/ChartModel.d.ts +2 -0
- package/build/types/cmp/dataview/DataViewModel.d.ts +2 -0
- package/build/types/cmp/daterange/DateRangePickerModel.d.ts +266 -0
- package/build/types/cmp/daterange/DateRangePresets.d.ts +26 -0
- package/build/types/cmp/daterange/DateRangeUtils.d.ts +96 -0
- package/build/types/cmp/daterange/Types.d.ts +152 -0
- package/build/types/cmp/daterange/index.d.ts +4 -0
- package/build/types/cmp/filter/FilterChooserModel.d.ts +3 -1
- package/build/types/cmp/form/FormModel.d.ts +3 -1
- package/build/types/cmp/grid/GridModel.d.ts +2 -0
- package/build/types/cmp/grid/Types.d.ts +2 -0
- package/build/types/cmp/grid/filter/GridFilterModel.d.ts +1 -1
- package/build/types/cmp/grouping/GroupingChooserModel.d.ts +3 -1
- package/build/types/cmp/input/HoistInputProps.d.ts +49 -2
- package/build/types/cmp/tab/TabContainerModel.d.ts +3 -1
- package/build/types/cmp/tab/TabModel.d.ts +3 -1
- package/build/types/cmp/treemap/TreeMapModel.d.ts +2 -0
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +2 -0
- package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +2 -0
- package/build/types/core/HoistBase.d.ts +7 -0
- package/build/types/core/HoistProps.d.ts +26 -1
- package/build/types/core/model/RootRefreshContextModel.d.ts +1 -0
- package/build/types/core/types/Telemetry.d.ts +1 -1
- package/build/types/data/Store.d.ts +3 -1
- package/build/types/data/StoreSelectionModel.d.ts +3 -1
- package/build/types/data/cube/Cube.d.ts +5 -2
- package/build/types/data/cube/View.d.ts +2 -0
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -0
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -0
- package/build/types/desktop/cmp/button/Button.d.ts +2 -2
- package/build/types/desktop/cmp/button/ButtonGroup.d.ts +2 -2
- package/build/types/desktop/cmp/dash/DashConfig.d.ts +2 -0
- package/build/types/desktop/cmp/dash/DashViewModel.d.ts +3 -1
- package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +1 -1
- package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -1
- package/build/types/desktop/cmp/daterange/DateRangePicker.d.ts +66 -0
- package/build/types/desktop/cmp/daterange/impl/CustomTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/DateRangePickerLocalModel.d.ts +121 -0
- package/build/types/desktop/cmp/daterange/impl/PeriodTab.d.ts +2 -0
- package/build/types/desktop/cmp/daterange/impl/PresetsTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/RelativeTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/TabUtils.d.ts +21 -0
- package/build/types/desktop/cmp/daterange/index.d.ts +2 -0
- package/build/types/desktop/cmp/dock/DockContainerModel.d.ts +3 -1
- package/build/types/desktop/cmp/dock/DockViewModel.d.ts +3 -1
- package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +4 -1
- package/build/types/desktop/cmp/input/IntentInput.d.ts +38 -0
- package/build/types/desktop/cmp/input/NumberInput.d.ts +2 -2
- package/build/types/desktop/cmp/input/SegmentedControl.d.ts +25 -4
- package/build/types/desktop/cmp/input/TextArea.d.ts +2 -2
- package/build/types/desktop/cmp/input/TextInput.d.ts +2 -2
- package/build/types/desktop/cmp/input/index.d.ts +1 -0
- package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +3 -1
- package/build/types/desktop/cmp/panel/PanelModel.d.ts +3 -1
- package/build/types/desktop/cmp/rest/RestGridModel.d.ts +1 -1
- package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/build/types/icon/Icon.d.ts +3 -0
- package/build/types/inspector/impl/InspectorUtils.d.ts +3 -0
- package/build/types/inspector/instances/InstancesModel.d.ts +4 -0
- package/build/types/kit/react-day-picker/index.d.ts +3 -3
- package/build/types/mobile/cmp/button/Button.d.ts +2 -2
- package/build/types/mobile/cmp/input/NumberInput.d.ts +2 -2
- package/build/types/mobile/cmp/input/SegmentedControl.d.ts +18 -4
- package/build/types/mobile/cmp/input/TextArea.d.ts +2 -2
- package/build/types/mobile/cmp/input/TextInput.d.ts +2 -2
- package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +3 -1
- package/build/types/mobile/cmp/navigator/PageModel.d.ts +3 -1
- package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/build/types/svc/InspectorService.d.ts +1 -0
- package/build/types/utils/js/LangUtils.d.ts +24 -5
- package/cmp/README.md +1 -0
- package/cmp/ag-grid/AgGridModel.ts +5 -0
- package/cmp/badge/Badge.ts +5 -3
- package/cmp/card/Card.ts +4 -3
- package/cmp/chart/ChartModel.ts +11 -1
- package/cmp/dataview/DataViewModel.ts +7 -1
- package/cmp/daterange/DateRangePickerModel.ts +628 -0
- package/cmp/daterange/DateRangePresets.ts +274 -0
- package/cmp/daterange/DateRangeUtils.ts +557 -0
- package/cmp/daterange/README.md +434 -0
- package/cmp/daterange/Types.ts +203 -0
- package/cmp/daterange/index.ts +10 -0
- package/cmp/filter/FilterChooserModel.ts +7 -4
- package/cmp/form/FormModel.ts +6 -0
- package/cmp/form/README.md +1 -0
- package/cmp/grid/GridModel.ts +24 -4
- package/cmp/grid/Types.ts +3 -0
- package/cmp/grid/filter/GridFilterModel.ts +3 -1
- package/cmp/grid/impl/RecordSortUtils.ts +10 -2
- package/cmp/grouping/GroupingChooserModel.ts +6 -1
- package/cmp/input/HoistInputProps.ts +50 -2
- package/cmp/input/README.md +42 -0
- package/cmp/layout/Box.ts +3 -2
- package/cmp/tab/TabContainerModel.ts +9 -1
- package/cmp/tab/TabModel.ts +5 -0
- package/cmp/treemap/SplitTreeMapModel.ts +12 -2
- package/cmp/treemap/TreeMapModel.ts +6 -1
- package/cmp/viewmanager/ViewManagerModel.ts +6 -1
- package/cmp/zoneGrid/ZoneGridModel.ts +7 -0
- package/core/HoistBase.ts +11 -0
- package/core/HoistProps.ts +27 -0
- package/core/README.md +46 -0
- package/core/impl/InstallServices.ts +5 -1
- package/core/model/RootRefreshContextModel.ts +2 -0
- package/core/types/Telemetry.ts +1 -1
- package/data/README.md +3 -1
- package/data/Store.ts +7 -0
- package/data/StoreSelectionModel.ts +5 -1
- package/data/cube/Cube.ts +12 -3
- package/data/cube/View.ts +5 -1
- package/desktop/README.md +2 -0
- package/desktop/appcontainer/LoginPanel.ts +2 -0
- package/desktop/cmp/button/Button.scss +71 -42
- package/desktop/cmp/button/Button.ts +10 -1
- package/desktop/cmp/button/ButtonGroup.ts +4 -1
- package/desktop/cmp/button/grid/ColChooserButton.ts +9 -1
- package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -0
- package/desktop/cmp/dash/DashConfig.ts +3 -0
- package/desktop/cmp/dash/DashViewModel.ts +14 -1
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +3 -1
- package/desktop/cmp/dash/container/DashContainerModel.ts +3 -1
- package/desktop/cmp/daterange/DateRangePicker.scss +577 -0
- package/desktop/cmp/daterange/DateRangePicker.ts +357 -0
- package/desktop/cmp/daterange/impl/CustomTab.ts +165 -0
- package/desktop/cmp/daterange/impl/DateRangePickerLocalModel.ts +482 -0
- package/desktop/cmp/daterange/impl/PeriodTab.ts +138 -0
- package/desktop/cmp/daterange/impl/PresetsTab.ts +58 -0
- package/desktop/cmp/daterange/impl/RelativeTab.ts +108 -0
- package/desktop/cmp/daterange/impl/TabUtils.ts +34 -0
- package/desktop/cmp/daterange/index.ts +8 -0
- package/desktop/cmp/dock/DockContainerModel.ts +6 -1
- package/desktop/cmp/dock/DockViewModel.ts +6 -1
- package/desktop/cmp/filechooser/FileChooser.ts +4 -0
- package/desktop/cmp/form/FormField.ts +1 -0
- package/desktop/cmp/grouping/GroupingChooser.ts +8 -2
- package/desktop/cmp/input/Checkbox.ts +1 -0
- package/desktop/cmp/input/CodeInput.ts +1 -0
- package/desktop/cmp/input/DateInput.scss +1 -1
- package/desktop/cmp/input/DateInput.ts +1 -0
- package/desktop/cmp/input/IntentInput.scss +202 -0
- package/desktop/cmp/input/IntentInput.ts +213 -0
- package/desktop/cmp/input/NumberInput.ts +11 -2
- package/desktop/cmp/input/Picker.ts +1 -0
- package/desktop/cmp/input/RadioInput.ts +2 -1
- package/desktop/cmp/input/SegmentedControl.scss +45 -2
- package/desktop/cmp/input/SegmentedControl.ts +66 -11
- package/desktop/cmp/input/Select.ts +9 -7
- package/desktop/cmp/input/Slider.ts +2 -0
- package/desktop/cmp/input/SwitchInput.ts +1 -0
- package/desktop/cmp/input/TextArea.ts +11 -2
- package/desktop/cmp/input/TextInput.ts +11 -2
- package/desktop/cmp/input/index.ts +1 -0
- package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +5 -0
- package/desktop/cmp/panel/Panel.ts +8 -5
- package/desktop/cmp/panel/PanelModel.ts +8 -0
- package/desktop/cmp/panel/impl/ResizeContainer.ts +2 -1
- package/desktop/cmp/rest/RestGridModel.ts +4 -0
- package/desktop/cmp/tab/impl/TabContainer.ts +2 -0
- package/docs/README.md +2 -0
- package/docs/build-and-publish.md +23 -7
- package/docs/doc-registry.json +8 -0
- package/docs/version-compatibility.md +2 -0
- package/icon/Icon.ts +9 -0
- package/icon/index.ts +24 -0
- package/inspector/README.md +8 -2
- package/inspector/impl/InspectorUtils.ts +12 -0
- package/inspector/instances/DiagnosticsModel.ts +3 -2
- package/inspector/instances/InstancesModel.ts +81 -14
- package/inspector/instances/InstancesPanel.ts +6 -0
- package/kit/react-day-picker/index.ts +3 -3
- package/mobile/appcontainer/LoginPanel.ts +2 -0
- package/mobile/cmp/button/Button.ts +16 -3
- package/mobile/cmp/form/FormField.ts +1 -0
- package/mobile/cmp/input/Checkbox.ts +1 -0
- package/mobile/cmp/input/DateInput.ts +1 -0
- package/mobile/cmp/input/Label.ts +13 -8
- package/mobile/cmp/input/NumberInput.ts +11 -2
- package/mobile/cmp/input/SearchInput.ts +1 -0
- package/mobile/cmp/input/SegmentedControl.scss +36 -6
- package/mobile/cmp/input/SegmentedControl.ts +37 -11
- package/mobile/cmp/input/Select.ts +2 -0
- package/mobile/cmp/input/SwitchInput.ts +1 -0
- package/mobile/cmp/input/TextArea.ts +11 -2
- package/mobile/cmp/input/TextInput.ts +11 -2
- package/mobile/cmp/navigator/NavigatorModel.ts +6 -1
- package/mobile/cmp/navigator/PageModel.ts +6 -1
- package/mobile/cmp/tab/impl/TabContainer.ts +9 -1
- package/package.json +19 -19
- package/styles/vars.scss +32 -2
- package/svc/InspectorService.ts +2 -0
- package/svc/TraceService.ts +5 -2
- package/utils/js/LangUtils.ts +37 -5
|
@@ -0,0 +1,357 @@
|
|
|
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 {DateRangePickerModel} from '@xh/hoist/cmp/daterange';
|
|
8
|
+
import {
|
|
9
|
+
DateRangePickerLocalModel,
|
|
10
|
+
type DateRangePickerTabSpec
|
|
11
|
+
} from './impl/DateRangePickerLocalModel';
|
|
12
|
+
import {box, div, filler, fragment, hbox, span, vbox} from '@xh/hoist/cmp/layout';
|
|
13
|
+
import {tabContainer} from '@xh/hoist/cmp/tab';
|
|
14
|
+
import {
|
|
15
|
+
hoistCmp,
|
|
16
|
+
type HoistProps,
|
|
17
|
+
type Intent,
|
|
18
|
+
type LayoutProps,
|
|
19
|
+
type TestSupportProps,
|
|
20
|
+
useLocalModel,
|
|
21
|
+
uses
|
|
22
|
+
} from '@xh/hoist/core';
|
|
23
|
+
import {button, type ButtonProps} from '@xh/hoist/desktop/cmp/button';
|
|
24
|
+
import '@xh/hoist/desktop/register';
|
|
25
|
+
import {Icon} from '@xh/hoist/icon';
|
|
26
|
+
import {popover} from '@xh/hoist/kit/blueprint';
|
|
27
|
+
import type {PopoverPosition} from '@blueprintjs/core';
|
|
28
|
+
import {elemWithin, getTestId} from '@xh/hoist/utils/js';
|
|
29
|
+
import {composeRefs, splitLayoutProps, useOnResize} from '@xh/hoist/utils/react';
|
|
30
|
+
import classNames from 'classnames';
|
|
31
|
+
import type {KeyboardEvent, ReactNode} from 'react';
|
|
32
|
+
import {customTab} from './impl/CustomTab';
|
|
33
|
+
import {periodTab} from './impl/PeriodTab';
|
|
34
|
+
import {presetsTab} from './impl/PresetsTab';
|
|
35
|
+
import {relativeTab} from './impl/RelativeTab';
|
|
36
|
+
import './DateRangePicker.scss';
|
|
37
|
+
|
|
38
|
+
export interface DateRangePickerProps
|
|
39
|
+
extends HoistProps<DateRangePickerModel>, LayoutProps, TestSupportProps {
|
|
40
|
+
/**
|
|
41
|
+
* Props forwarded to the trigger button - use to customize its `icon`, `intent`, `minimal`,
|
|
42
|
+
* `outlined`, `tooltip`, or any other {@link ButtonProps}.
|
|
43
|
+
*/
|
|
44
|
+
buttonProps?: Partial<ButtonProps>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Text rendered in the popover footer, or null to suppress. Default is a note on the date that
|
|
48
|
+
* periods are relative to. Not shown by a single-tab picker, whose footer carries the resolved
|
|
49
|
+
* dates instead.
|
|
50
|
+
*/
|
|
51
|
+
footerNote?: ReactNode;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Intent for the popover's selection accent - the highlight on selected presets, months,
|
|
55
|
+
* and calendar days, and the Apply button. Also applied to the trigger button unless
|
|
56
|
+
* overridden via `buttonProps`. Default: the primary intent for the accent, none for the
|
|
57
|
+
* trigger.
|
|
58
|
+
*/
|
|
59
|
+
intent?: Intent;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Placement of the popover relative to the trigger. Default 'bottom-left', which flips to
|
|
63
|
+
* 'top-left' when there is no room below - e.g. from a bottom toolbar - and holds the popover's
|
|
64
|
+
* left edge to the trigger as the trigger's label changes width.
|
|
65
|
+
*/
|
|
66
|
+
popoverPosition?: PopoverPosition;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* True (default) to show the resolved dates beside the period label on the trigger. The dates
|
|
70
|
+
* are also dropped automatically when a stretched trigger (e.g. `flex: 1` within a narrow
|
|
71
|
+
* dashboard widget) measures too narrow to fit them - either way, they remain available via
|
|
72
|
+
* the trigger's tooltip.
|
|
73
|
+
*/
|
|
74
|
+
showRange?: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* True to flank the trigger with previous/next buttons that move the applied range by its own
|
|
78
|
+
* length - see {@link DateRangePickerModel.stepRange}. Default false.
|
|
79
|
+
*/
|
|
80
|
+
showStepButtons?: boolean;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* True (default) to style trigger button background and borders to match inputs. Set to
|
|
84
|
+
* false to render an outlined button instead.
|
|
85
|
+
*/
|
|
86
|
+
styleButtonAsInput?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A dropdown control for selecting a period of time - one compact trigger that can express
|
|
91
|
+
* presets (e.g. MTD, Prev 30 Days), relative lookbacks, calendar months, quarters and years, and
|
|
92
|
+
* custom ranges of dates.
|
|
93
|
+
*
|
|
94
|
+
* The trigger shows the applied period's label with its resolved dates, and opens a popover with a
|
|
95
|
+
* tab for each of those selection shapes. The backing {@link DateRangePickerModel}'s `tabs` config
|
|
96
|
+
* selects which tabs show. Preset and period picks commit on click. Relative and custom picks are
|
|
97
|
+
* drafts until Apply, held in a local model that never touches the applied value.
|
|
98
|
+
*
|
|
99
|
+
* App code constructs and persists the {@link DateRangePickerModel}, which owns the applied value
|
|
100
|
+
* and the ranges and filters it resolves to - see that class for the full API.
|
|
101
|
+
*
|
|
102
|
+
* @see DateRangePickerModel
|
|
103
|
+
*/
|
|
104
|
+
export const [DateRangePicker, dateRangePicker] = hoistCmp.withFactory<DateRangePickerProps>({
|
|
105
|
+
displayName: 'DateRangePicker',
|
|
106
|
+
model: uses(DateRangePickerModel),
|
|
107
|
+
className: 'xh-date-range-picker',
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
{
|
|
111
|
+
model,
|
|
112
|
+
className,
|
|
113
|
+
buttonProps,
|
|
114
|
+
footerNote,
|
|
115
|
+
intent,
|
|
116
|
+
popoverPosition = 'bottom-left',
|
|
117
|
+
showRange = true,
|
|
118
|
+
showStepButtons = false,
|
|
119
|
+
styleButtonAsInput = true,
|
|
120
|
+
testId,
|
|
121
|
+
...props
|
|
122
|
+
},
|
|
123
|
+
ref
|
|
124
|
+
) {
|
|
125
|
+
const impl = useLocalModel(() => new DateRangePickerLocalModel(testId, TAB_SPECS)),
|
|
126
|
+
[layoutProps] = splitLayoutProps(props),
|
|
127
|
+
{singleTab} = impl,
|
|
128
|
+
// A stretched trigger adapts to its measured width - a content-sized toolbar trigger
|
|
129
|
+
// never truncates, so it is not measured at all.
|
|
130
|
+
{flex, width} = layoutProps,
|
|
131
|
+
stretched =
|
|
132
|
+
(flex != null && flex !== 0 && flex !== '0' && flex !== 'none') || width != null,
|
|
133
|
+
compact = stretched && impl.measuredWidth != null && impl.measuredWidth < 280,
|
|
134
|
+
resizeRef = useOnResize(rect => (impl.measuredWidth = rect.width), {debounce: 100}),
|
|
135
|
+
viewRef = stretched ? composeRefs(impl.viewRef, resizeRef) : impl.viewRef;
|
|
136
|
+
|
|
137
|
+
impl.intent = intent;
|
|
138
|
+
|
|
139
|
+
return box({
|
|
140
|
+
className: classNames(className, compact && 'xh-date-range-picker--compact'),
|
|
141
|
+
ref: ref ? composeRefs(ref, viewRef) : viewRef,
|
|
142
|
+
...layoutProps,
|
|
143
|
+
items: [
|
|
144
|
+
stepButton({
|
|
145
|
+
omit: !showStepButtons,
|
|
146
|
+
model: impl,
|
|
147
|
+
steps: -1,
|
|
148
|
+
styleButtonAsInput,
|
|
149
|
+
testId
|
|
150
|
+
}),
|
|
151
|
+
popover({
|
|
152
|
+
isOpen: impl.isOpen,
|
|
153
|
+
position: popoverPosition,
|
|
154
|
+
minimal: false,
|
|
155
|
+
popoverClassName: classNames(
|
|
156
|
+
'xh-date-range-picker-popover',
|
|
157
|
+
singleTab && 'xh-date-range-picker-popover--single',
|
|
158
|
+
intent && `xh-date-range-picker-popover--intent-${intent}`
|
|
159
|
+
),
|
|
160
|
+
item: trigger({
|
|
161
|
+
model: impl,
|
|
162
|
+
testId,
|
|
163
|
+
intent,
|
|
164
|
+
showRange: showRange && !compact,
|
|
165
|
+
styleButtonAsInput,
|
|
166
|
+
buttonProps
|
|
167
|
+
}),
|
|
168
|
+
content: popoverContent({model: impl, testId, intent, footerNote}),
|
|
169
|
+
onInteraction: (willOpen, e) => {
|
|
170
|
+
if (!willOpen && impl.isOpen) {
|
|
171
|
+
// Clicks on this instance's own trigger toggle via its onClick - don't
|
|
172
|
+
// double-close. Anything else, including this picker's own step
|
|
173
|
+
// buttons, closes the popover so it never shows drafts seeded from a
|
|
174
|
+
// stale value.
|
|
175
|
+
const target = e?.target as HTMLElement,
|
|
176
|
+
onTrigger =
|
|
177
|
+
target && elemWithin(target, 'xh-date-range-picker__trigger');
|
|
178
|
+
if (!onTrigger) impl.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}),
|
|
182
|
+
stepButton({
|
|
183
|
+
omit: !showStepButtons,
|
|
184
|
+
model: impl,
|
|
185
|
+
steps: 1,
|
|
186
|
+
styleButtonAsInput,
|
|
187
|
+
testId
|
|
188
|
+
})
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
//------------------
|
|
195
|
+
// Step buttons
|
|
196
|
+
//------------------
|
|
197
|
+
const stepButton = hoistCmp.factory<DateRangePickerLocalModel>(
|
|
198
|
+
({model, steps, styleButtonAsInput, testId}) => {
|
|
199
|
+
const {parentModel} = model,
|
|
200
|
+
back = steps < 0;
|
|
201
|
+
return button({
|
|
202
|
+
className: 'xh-date-range-picker__step-btn',
|
|
203
|
+
icon: back ? Icon.angleLeft() : Icon.angleRight(),
|
|
204
|
+
...(styleButtonAsInput ? {minimal: true} : {outlined: true}),
|
|
205
|
+
title: back ? 'Previous period' : 'Next period',
|
|
206
|
+
disabled: back ? !parentModel.canStepBack : !parentModel.canStepForward,
|
|
207
|
+
testId: getTestId(testId, back ? 'step-back' : 'step-forward'),
|
|
208
|
+
onClick: () => parentModel.stepRange(steps)
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
//------------------
|
|
214
|
+
// Trigger
|
|
215
|
+
//------------------
|
|
216
|
+
const trigger = hoistCmp.factory<DateRangePickerLocalModel>(
|
|
217
|
+
({model, testId, intent, showRange, styleButtonAsInput, buttonProps}) => {
|
|
218
|
+
const {parentModel} = model,
|
|
219
|
+
{label, rangeLabel, labelNeedsDates} = parentModel,
|
|
220
|
+
// Without its dates, `Custom` or `As Of` says nothing - show the dates in their place.
|
|
221
|
+
text = !showRange && labelNeedsDates ? rangeLabel : label,
|
|
222
|
+
base = 'xh-date-range-picker__trigger';
|
|
223
|
+
|
|
224
|
+
return button({
|
|
225
|
+
// Input mode relies on the `--as-input` class for its chrome, so renders minimal.
|
|
226
|
+
...(styleButtonAsInput ? {minimal: true} : {outlined: true}),
|
|
227
|
+
intent,
|
|
228
|
+
icon: Icon.calendar(),
|
|
229
|
+
title: `${label} | ${rangeLabel}`,
|
|
230
|
+
...buttonProps,
|
|
231
|
+
className: classNames(
|
|
232
|
+
base,
|
|
233
|
+
styleButtonAsInput && `${base}--as-input`,
|
|
234
|
+
buttonProps?.className
|
|
235
|
+
),
|
|
236
|
+
active: model.isOpen,
|
|
237
|
+
testId: getTestId(testId, 'trigger'),
|
|
238
|
+
items: [
|
|
239
|
+
span({className: `${base}-label`, item: text}),
|
|
240
|
+
span({omit: !showRange, className: `${base}-divider`, item: '|'}),
|
|
241
|
+
span({omit: !showRange, className: `${base}-range`, item: rangeLabel})
|
|
242
|
+
],
|
|
243
|
+
onClick: () => model.toggleOpen(),
|
|
244
|
+
// Arrow keys step the period without opening the popover.
|
|
245
|
+
onKeyDown: (e: KeyboardEvent) => {
|
|
246
|
+
const steps = e.key === 'ArrowLeft' ? -1 : e.key === 'ArrowRight' ? 1 : 0;
|
|
247
|
+
if (steps) {
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
parentModel.stepRange(steps);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
//------------------
|
|
257
|
+
// Popover shell
|
|
258
|
+
//------------------
|
|
259
|
+
const popoverContent = hoistCmp.factory<DateRangePickerLocalModel>(
|
|
260
|
+
({model, testId, intent, footerNote}) => {
|
|
261
|
+
return vbox({
|
|
262
|
+
className: 'xh-date-range-picker-popover__content',
|
|
263
|
+
testId: getTestId(testId, 'popover'),
|
|
264
|
+
onKeyDown: e => {
|
|
265
|
+
// Enter applies the active draft - except on buttons, whose Enter is their click.
|
|
266
|
+
const onButton = !!(e.target as HTMLElement).closest('button');
|
|
267
|
+
if (e.key === 'Enter' && model.applyEnabled && !onButton) model.apply();
|
|
268
|
+
},
|
|
269
|
+
items: [
|
|
270
|
+
tabContainer({
|
|
271
|
+
className: 'xh-date-range-picker-popover__body',
|
|
272
|
+
model: model.tabModel,
|
|
273
|
+
// Switcher testId seeds each tab's own - `<testId>-rail-<tabId>`.
|
|
274
|
+
switcher: model.singleTab
|
|
275
|
+
? false
|
|
276
|
+
: {orientation: 'left', testId: getTestId(testId, 'rail')}
|
|
277
|
+
}),
|
|
278
|
+
footer({model, testId, intent, footerNote})
|
|
279
|
+
]
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
//------------------
|
|
285
|
+
// Footer
|
|
286
|
+
//------------------
|
|
287
|
+
const footer = hoistCmp.factory<DateRangePickerLocalModel>(
|
|
288
|
+
({model, testId, intent, footerNote}) => {
|
|
289
|
+
const {parentModel, singleTab} = model,
|
|
290
|
+
// Explicit null suppresses the note; undefined takes the default.
|
|
291
|
+
note = footerNote === undefined ? anchorNote({model}) : footerNote,
|
|
292
|
+
showNote = !singleTab && note != null && note !== false,
|
|
293
|
+
// Apply and Cancel only where there is a draft to apply - presets and periods commit
|
|
294
|
+
// on click, and drafts commit as they change under `commitOnChange`.
|
|
295
|
+
showControls = model.showApplyControls && model.applyEnabled;
|
|
296
|
+
|
|
297
|
+
// A single tab with nothing to apply - or that applies as it changes - needs no buttons.
|
|
298
|
+
if (singleTab && !showControls) {
|
|
299
|
+
return div({
|
|
300
|
+
className: 'xh-date-range-picker-popover__single-footer',
|
|
301
|
+
item: parentModel.rangeLabel
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!showNote && !showControls) return null;
|
|
306
|
+
|
|
307
|
+
return hbox({
|
|
308
|
+
className: 'xh-date-range-picker-popover__footer',
|
|
309
|
+
items: [
|
|
310
|
+
Icon.info({
|
|
311
|
+
omit: !showNote,
|
|
312
|
+
className: 'xh-date-range-picker-popover__footer-icon'
|
|
313
|
+
}),
|
|
314
|
+
div({
|
|
315
|
+
omit: !showNote,
|
|
316
|
+
className: 'xh-date-range-picker-popover__footer-note',
|
|
317
|
+
item: note
|
|
318
|
+
}),
|
|
319
|
+
filler(),
|
|
320
|
+
button({
|
|
321
|
+
omit: !showControls,
|
|
322
|
+
text: 'Cancel',
|
|
323
|
+
testId: getTestId(testId, 'cancel'),
|
|
324
|
+
onClick: () => model.close()
|
|
325
|
+
}),
|
|
326
|
+
button({
|
|
327
|
+
omit: !showControls,
|
|
328
|
+
text: 'Apply',
|
|
329
|
+
outlined: true,
|
|
330
|
+
intent: intent ?? 'primary',
|
|
331
|
+
testId: getTestId(testId, 'apply'),
|
|
332
|
+
onClick: () => model.apply()
|
|
333
|
+
})
|
|
334
|
+
]
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
/** The default footer note - muted prose, with the date it names set apart as on the trigger. */
|
|
340
|
+
const anchorNote = hoistCmp.factory<DateRangePickerLocalModel>(({model}) => {
|
|
341
|
+
const {prefix, date} = model.anchorNote;
|
|
342
|
+
return fragment(
|
|
343
|
+
prefix,
|
|
344
|
+
span({className: 'xh-date-range-picker-popover__footer-date', item: date})
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
//------------------
|
|
349
|
+
// Tabs
|
|
350
|
+
//------------------
|
|
351
|
+
/** Switcher entry + content for each tab. The model's `tabs` config selects and orders them. */
|
|
352
|
+
const TAB_SPECS: DateRangePickerTabSpec[] = [
|
|
353
|
+
{id: 'presets', title: 'Presets', icon: Icon.bolt(), content: presetsTab},
|
|
354
|
+
{id: 'relative', title: 'Relative', icon: Icon.history(), content: relativeTab},
|
|
355
|
+
{id: 'period', title: 'Months & Years', icon: Icon.calendarDays(), content: periodTab},
|
|
356
|
+
{id: 'custom', title: 'Custom Range', icon: Icon.calendarRange(), content: customTab}
|
|
357
|
+
];
|
|
@@ -0,0 +1,165 @@
|
|
|
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 type {DateRangePickerLocalModel} from './DateRangePickerLocalModel';
|
|
8
|
+
import {div, hbox, span, vbox} from '@xh/hoist/cmp/layout';
|
|
9
|
+
import {hoistCmp, type HoistProps} from '@xh/hoist/core';
|
|
10
|
+
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
11
|
+
import {dateInput} from '@xh/hoist/desktop/cmp/input';
|
|
12
|
+
import {Icon} from '@xh/hoist/icon';
|
|
13
|
+
import type {LocalDate} from '@xh/hoist/utils/datetime';
|
|
14
|
+
import {getTestId} from '@xh/hoist/utils/js';
|
|
15
|
+
import classNames from 'classnames';
|
|
16
|
+
import {isString} from 'lodash';
|
|
17
|
+
import {clickable, dataAttrs} from './TabUtils';
|
|
18
|
+
|
|
19
|
+
/** Custom tab - start and end inputs over a two-month calendar, drafted until applied. */
|
|
20
|
+
export const customTab = hoistCmp.factory<DateRangePickerLocalModel>(({model, testId}) => {
|
|
21
|
+
const {parentModel, minDate, maxDate, leftMonth, rightMonth} = model;
|
|
22
|
+
|
|
23
|
+
const field = (edge: 'start' | 'end') =>
|
|
24
|
+
vbox({
|
|
25
|
+
className: classNames(
|
|
26
|
+
'xh-date-range-picker-popover__field',
|
|
27
|
+
model.nextEdge === edge && 'xh-date-range-picker-popover__field--armed'
|
|
28
|
+
),
|
|
29
|
+
items: [
|
|
30
|
+
div({
|
|
31
|
+
className: 'xh-date-range-picker-popover__field-label',
|
|
32
|
+
item: edge === 'start' ? 'Start' : 'End'
|
|
33
|
+
}),
|
|
34
|
+
dateInput({
|
|
35
|
+
value: edge === 'start' ? model.customStart : model.customEnd,
|
|
36
|
+
onCommit: v =>
|
|
37
|
+
edge === 'start' ? model.commitStartInput(v) : model.commitEndInput(v),
|
|
38
|
+
valueType: 'localDate',
|
|
39
|
+
// Inputs need a parseable string format - a function format cannot be typed into.
|
|
40
|
+
formatString: isString(parentModel.dateFormat)
|
|
41
|
+
? parentModel.dateFormat
|
|
42
|
+
: 'YYYY-MM-DD',
|
|
43
|
+
enablePicker: false,
|
|
44
|
+
rightElement: null,
|
|
45
|
+
width: '100%',
|
|
46
|
+
minDate,
|
|
47
|
+
maxDate,
|
|
48
|
+
testId: getTestId(testId, `${edge}-input`)
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return vbox({
|
|
54
|
+
className: 'xh-date-range-picker-popover__tab',
|
|
55
|
+
items: [
|
|
56
|
+
hbox({
|
|
57
|
+
className: 'xh-date-range-picker-popover__custom-fields',
|
|
58
|
+
items: [
|
|
59
|
+
field('start'),
|
|
60
|
+
Icon.arrowRight({className: 'xh-date-range-picker-popover__field-arrow'}),
|
|
61
|
+
field('end')
|
|
62
|
+
]
|
|
63
|
+
}),
|
|
64
|
+
hbox({
|
|
65
|
+
className: 'xh-date-range-picker-popover__cal-nav',
|
|
66
|
+
items: [
|
|
67
|
+
button({
|
|
68
|
+
className: 'xh-date-range-picker-popover__nav-btn',
|
|
69
|
+
icon: Icon.angleLeft(),
|
|
70
|
+
minimal: true,
|
|
71
|
+
disabled: !model.canCalBack,
|
|
72
|
+
testId: getTestId(testId, 'cal-back'),
|
|
73
|
+
onClick: () => model.stepCalendar(-1)
|
|
74
|
+
}),
|
|
75
|
+
span({
|
|
76
|
+
className: 'xh-date-range-picker-popover__cal-title',
|
|
77
|
+
item: leftMonth.format('MMMM YYYY')
|
|
78
|
+
}),
|
|
79
|
+
span({
|
|
80
|
+
className: 'xh-date-range-picker-popover__cal-title',
|
|
81
|
+
item: rightMonth.format('MMMM YYYY')
|
|
82
|
+
}),
|
|
83
|
+
button({
|
|
84
|
+
className: 'xh-date-range-picker-popover__nav-btn',
|
|
85
|
+
icon: Icon.angleRight(),
|
|
86
|
+
minimal: true,
|
|
87
|
+
disabled: !model.canCalForward,
|
|
88
|
+
testId: getTestId(testId, 'cal-forward'),
|
|
89
|
+
onClick: () => model.stepCalendar(1)
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
}),
|
|
93
|
+
hbox({
|
|
94
|
+
className: 'xh-date-range-picker-popover__cals',
|
|
95
|
+
items: [
|
|
96
|
+
calendarMonth({model, monthStart: leftMonth}),
|
|
97
|
+
calendarMonth({model, monthStart: rightMonth})
|
|
98
|
+
]
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const DOW_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
105
|
+
|
|
106
|
+
interface CalendarMonthProps extends HoistProps<DateRangePickerLocalModel> {
|
|
107
|
+
monthStart: LocalDate;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const calendarMonth = hoistCmp.factory<CalendarMonthProps>(({model, monthStart}) => {
|
|
111
|
+
const {customStart, customEnd} = model;
|
|
112
|
+
|
|
113
|
+
return div({
|
|
114
|
+
className: 'xh-date-range-picker-popover__cal',
|
|
115
|
+
items: [
|
|
116
|
+
div({
|
|
117
|
+
className: 'xh-date-range-picker-popover__cal-dow',
|
|
118
|
+
items: DOW_LABELS.map((it, idx) => span({key: idx, item: it}))
|
|
119
|
+
}),
|
|
120
|
+
...buildWeeks(monthStart).map((week, wIdx) =>
|
|
121
|
+
div({
|
|
122
|
+
key: wIdx,
|
|
123
|
+
className: 'xh-date-range-picker-popover__cal-week',
|
|
124
|
+
items: week.map((day, dIdx) => {
|
|
125
|
+
if (!day) {
|
|
126
|
+
return div({
|
|
127
|
+
key: dIdx,
|
|
128
|
+
className: 'xh-date-range-picker-popover__cal-cell'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const disabled = model.isDayDisabled(day),
|
|
132
|
+
edge = day === customStart || day === customEnd,
|
|
133
|
+
inRange = day > customStart && day < customEnd;
|
|
134
|
+
return div({
|
|
135
|
+
key: dIdx,
|
|
136
|
+
className: classNames(
|
|
137
|
+
'xh-date-range-picker-popover__cal-cell',
|
|
138
|
+
edge && 'xh-date-range-picker-popover__cal-cell--edge',
|
|
139
|
+
inRange && 'xh-date-range-picker-popover__cal-cell--in-range',
|
|
140
|
+
disabled && 'xh-date-range-picker-popover__cal-cell--disabled'
|
|
141
|
+
),
|
|
142
|
+
...dataAttrs({'data-date': day.isoString}),
|
|
143
|
+
item: day.format('D'),
|
|
144
|
+
...clickable(disabled ? null : () => model.pickDay(day))
|
|
145
|
+
});
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
)
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function buildWeeks(monthStart: LocalDate): (LocalDate | null)[][] {
|
|
154
|
+
const startOffset = monthStart.moment.day(),
|
|
155
|
+
daysInMonth = monthStart.endOfMonth().diff(monthStart, 'days') + 1,
|
|
156
|
+
cells: (LocalDate | null)[] = [];
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < startOffset; i++) cells.push(null);
|
|
159
|
+
for (let d = 0; d < daysInMonth; d++) cells.push(monthStart.add(d, 'days'));
|
|
160
|
+
while (cells.length % 7 !== 0) cells.push(null);
|
|
161
|
+
|
|
162
|
+
const weeks = [];
|
|
163
|
+
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
|
|
164
|
+
return weeks;
|
|
165
|
+
}
|