@xh/hoist 79.0.0-SNAPSHOT.1766020485210 → 79.0.0-SNAPSHOT.1766097863558
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 +16 -0
- package/admin/AppComponent.ts +9 -1
- package/admin/AppModel.ts +0 -4
- package/admin/tabs/cluster/instances/InstancesTab.ts +1 -1
- package/admin/tabs/cluster/instances/InstancesTabModel.ts +0 -1
- package/admin/tabs/userData/roles/details/RoleDetailsModel.ts +0 -1
- package/build/types/cmp/tab/TabContainer.d.ts +19 -4
- package/build/types/cmp/tab/TabContainerModel.d.ts +18 -19
- package/build/types/cmp/tab/Types.d.ts +61 -0
- package/build/types/cmp/tab/index.d.ts +1 -1
- package/build/types/core/elem.d.ts +3 -3
- package/build/types/data/RecordAction.d.ts +4 -1
- package/build/types/desktop/cmp/dash/canvas/DashCanvas.d.ts +3 -2
- package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +45 -3
- package/build/types/desktop/cmp/panel/Panel.d.ts +2 -2
- package/build/types/desktop/cmp/rest/RestGrid.d.ts +3 -3
- package/build/types/desktop/cmp/tab/TabSwitcher.d.ts +1 -1
- package/build/types/desktop/cmp/tab/dynamic/DynamicTabSwitcher.d.ts +7 -0
- package/build/types/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel.d.ts +30 -0
- package/build/types/desktop/cmp/tab/dynamic/scroller/Scroller.d.ts +19 -0
- package/build/types/desktop/cmp/tab/dynamic/scroller/ScrollerModel.d.ts +23 -0
- package/build/types/desktop/cmp/tab/impl/Tab.d.ts +7 -2
- package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/build/types/desktop/cmp/tab/impl/TabContextMenuItems.d.ts +4 -0
- package/build/types/desktop/cmp/tab/index.d.ts +1 -0
- package/build/types/dynamics/desktop.d.ts +1 -0
- package/build/types/mobile/cmp/panel/Panel.d.ts +2 -2
- package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/cmp/tab/TabContainer.ts +19 -4
- package/cmp/tab/TabContainerModel.ts +113 -54
- package/cmp/tab/TabModel.ts +1 -2
- package/cmp/tab/Types.ts +80 -0
- package/cmp/tab/index.ts +1 -1
- package/core/elem.ts +5 -5
- package/data/RecordAction.ts +4 -1
- package/desktop/appcontainer/AppContainer.ts +3 -2
- package/desktop/cmp/dash/canvas/DashCanvas.ts +57 -35
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +135 -21
- package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilter.ts +1 -1
- package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.ts +0 -1
- package/desktop/cmp/panel/Panel.ts +2 -2
- package/desktop/cmp/rest/RestGrid.ts +4 -5
- package/desktop/cmp/tab/TabSwitcher.ts +18 -3
- package/desktop/cmp/tab/Tabs.scss +1 -0
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.scss +53 -0
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.ts +237 -0
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel.ts +167 -0
- package/desktop/cmp/tab/dynamic/scroller/Scroller.ts +69 -0
- package/desktop/cmp/tab/dynamic/scroller/ScrollerModel.ts +92 -0
- package/desktop/cmp/tab/impl/Tab.ts +30 -6
- package/desktop/cmp/tab/impl/TabContainer.ts +34 -9
- package/desktop/cmp/tab/impl/TabContextMenuItems.ts +21 -0
- package/desktop/cmp/tab/index.ts +1 -0
- package/dynamics/desktop.ts +2 -0
- package/mobile/cmp/panel/Panel.ts +2 -2
- package/mobile/cmp/tab/impl/TabContainer.ts +16 -9
- package/package.json +2 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/build/types/cmp/tab/TabSwitcherProps.d.ts +0 -16
- package/cmp/tab/TabSwitcherProps.ts +0 -28
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {TabContainerModel, TabModel} from '@xh/hoist/cmp/tab';
|
|
2
|
+
import {
|
|
3
|
+
IDynamicTabSwitcherModel,
|
|
4
|
+
TabSwitcherConfig,
|
|
5
|
+
TabSwitcherMenuContext
|
|
6
|
+
} from '@xh/hoist/cmp/tab/Types';
|
|
7
|
+
import {HoistModel, MenuItemLike, MenuToken, ReactionSpec, XH} from '@xh/hoist/core';
|
|
8
|
+
import {getContextMenuItem} from '@xh/hoist/desktop/cmp/tab/impl/TabContextMenuItems';
|
|
9
|
+
import {Icon} from '@xh/hoist/icon';
|
|
10
|
+
import {makeObservable} from '@xh/hoist/mobx';
|
|
11
|
+
import {compact, find} from 'lodash';
|
|
12
|
+
import {action, computed, observable, when} from 'mobx';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* State management for the DynamicTabSwitcher component.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export class DynamicTabSwitcherModel extends HoistModel implements IDynamicTabSwitcherModel {
|
|
20
|
+
declare config: TabSwitcherConfig;
|
|
21
|
+
|
|
22
|
+
private readonly extraMenuItems: Array<MenuItemLike<MenuToken, TabSwitcherMenuContext>>;
|
|
23
|
+
private readonly tabContainerModel: TabContainerModel;
|
|
24
|
+
@observable.ref private visibleTabState: TabState[];
|
|
25
|
+
|
|
26
|
+
@computed
|
|
27
|
+
get favoriteTabIds(): string[] {
|
|
28
|
+
return this.visibleTabState.filter(it => it.isFavorite).map(it => it.tabId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@computed
|
|
32
|
+
get visibleTabs(): TabModel[] {
|
|
33
|
+
return compact(this.visibleTabState.map(it => this.tabContainerModel.findTab(it.tabId)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@computed
|
|
37
|
+
get enabledVisibleTabs(): TabModel[] {
|
|
38
|
+
return this.visibleTabs.filter(it => !it.disabled);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
{extraMenuItems = [], initialFavorites = []}: TabSwitcherConfig,
|
|
43
|
+
tabContainerModel: TabContainerModel
|
|
44
|
+
) {
|
|
45
|
+
super();
|
|
46
|
+
makeObservable(this);
|
|
47
|
+
|
|
48
|
+
this.extraMenuItems = extraMenuItems;
|
|
49
|
+
this.tabContainerModel = tabContainerModel;
|
|
50
|
+
this.visibleTabState = this.getValidTabIds(initialFavorites).map(tabId => ({
|
|
51
|
+
tabId,
|
|
52
|
+
isFavorite: true
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Wait for router to start before observing active tab
|
|
56
|
+
when(
|
|
57
|
+
() => XH.appIsRunning,
|
|
58
|
+
() => this.addReaction(this.activeTabReaction())
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
isTabActive(tabId: string): boolean {
|
|
63
|
+
return this.tabContainerModel.activeTabId === tabId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
isTabFavorite(tabId: string): boolean {
|
|
67
|
+
return !!find(this.visibleTabState, {tabId})?.isFavorite;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@action
|
|
71
|
+
toggleTabFavorite(tabId: string) {
|
|
72
|
+
this.visibleTabState = !this.isTabVisible(tabId)
|
|
73
|
+
? [...this.visibleTabState, {tabId, isFavorite: true}]
|
|
74
|
+
: this.visibleTabState.map(it =>
|
|
75
|
+
it.tabId === tabId ? {tabId, isFavorite: !this.isTabFavorite(tabId)} : it
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
activate(tabId: string) {
|
|
80
|
+
this.tabContainerModel.activateTab(tabId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@action
|
|
84
|
+
hide(tabId: string) {
|
|
85
|
+
const {enabledVisibleTabs, tabContainerModel} = this;
|
|
86
|
+
if (tabContainerModel.activeTabId === tabId) {
|
|
87
|
+
const visitableTabs = enabledVisibleTabs.filter(tab => tab instanceof TabModel),
|
|
88
|
+
activeTabIdx = visitableTabs.findIndex(tab => tab.id === tabId),
|
|
89
|
+
toActivate =
|
|
90
|
+
visitableTabs[
|
|
91
|
+
activeTabIdx + (activeTabIdx === visitableTabs.length - 1 ? -1 : 1)
|
|
92
|
+
];
|
|
93
|
+
if (toActivate) tabContainerModel.activateTab(toActivate);
|
|
94
|
+
}
|
|
95
|
+
this.visibleTabState = this.visibleTabState.filter(it => it.tabId !== tabId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getContextMenuItems(
|
|
99
|
+
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
|
100
|
+
tab: TabModel
|
|
101
|
+
): Array<MenuItemLike<MenuToken, TabSwitcherMenuContext>> {
|
|
102
|
+
const isFavorite = this.isTabFavorite(tab.id);
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
icon: Icon.favorite({prefix: isFavorite ? 'fal' : 'fas'}),
|
|
106
|
+
text: isFavorite ? 'Remove from Favorites' : 'Add to Favorites',
|
|
107
|
+
actionFn: () => this.toggleTabFavorite(tab.id)
|
|
108
|
+
},
|
|
109
|
+
...this.extraMenuItems.map(item => getContextMenuItem(item, {contextMenuEvent: e, tab}))
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@action
|
|
114
|
+
setFavoriteTabIds(tabIds: string[]) {
|
|
115
|
+
const visibleTabState = this.getValidTabIds(tabIds).map(tabId => ({
|
|
116
|
+
tabId,
|
|
117
|
+
isFavorite: true
|
|
118
|
+
})),
|
|
119
|
+
{activeTab} = this.tabContainerModel;
|
|
120
|
+
if (activeTab && !activeTab.excludeFromSwitcher && !tabIds.includes(activeTab.id)) {
|
|
121
|
+
visibleTabState.push({tabId: activeTab.id, isFavorite: false});
|
|
122
|
+
}
|
|
123
|
+
this.visibleTabState = visibleTabState;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@action
|
|
127
|
+
onDragEnd(result) {
|
|
128
|
+
if (!result.destination) return;
|
|
129
|
+
const visibleTabState = [...this.visibleTabState],
|
|
130
|
+
[removed] = visibleTabState.splice(result.source.index, 1);
|
|
131
|
+
visibleTabState.splice(result.destination.index, 0, removed);
|
|
132
|
+
this.visibleTabState = visibleTabState;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// -------------------------------
|
|
136
|
+
// Implementation
|
|
137
|
+
// -------------------------------
|
|
138
|
+
private activeTabReaction(): ReactionSpec<TabModel> {
|
|
139
|
+
return {
|
|
140
|
+
track: () => this.tabContainerModel.activeTab,
|
|
141
|
+
run: ({id: tabId, excludeFromSwitcher}) => {
|
|
142
|
+
if (!excludeFromSwitcher && !this.isTabVisible(tabId)) {
|
|
143
|
+
this.visibleTabState = [...this.visibleTabState, {tabId, isFavorite: false}];
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
fireImmediately: true
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private getValidTabIds(tabIds: string[]): string[] {
|
|
151
|
+
return tabIds.filter(id => this.isValidTabId(id));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private isValidTabId(tabId: string): boolean {
|
|
155
|
+
const tabModel = this.tabContainerModel.findTab(tabId);
|
|
156
|
+
return !!(tabModel && !tabModel.excludeFromSwitcher);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private isTabVisible(tabId: string): boolean {
|
|
160
|
+
return this.visibleTabState.some(it => it.tabId === tabId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface TabState {
|
|
165
|
+
tabId: string;
|
|
166
|
+
isFavorite: boolean;
|
|
167
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import composeRefs from '@seznam/compose-react-refs';
|
|
2
|
+
import {hbox, vbox} from '@xh/hoist/cmp/layout';
|
|
3
|
+
import {BoxProps, creates, hoistCmp, HoistProps} from '@xh/hoist/core';
|
|
4
|
+
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
5
|
+
import {ScrollerModel} from '@xh/hoist/desktop/cmp/tab/dynamic/scroller/ScrollerModel';
|
|
6
|
+
import {Icon} from '@xh/hoist/icon';
|
|
7
|
+
import {useOnResize} from '@xh/hoist/utils/react';
|
|
8
|
+
import React, {Ref} from 'react';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A scroller component that displays a content component with directional scroll buttons when the
|
|
12
|
+
* content overflows the viewport.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface ScrollerProps extends HoistProps<ScrollerModel>, Omit<BoxProps, 'content'> {
|
|
17
|
+
/** The content to be displayed within the scroller. */
|
|
18
|
+
content: React.FC<{ref: Ref<HTMLDivElement>}>;
|
|
19
|
+
/** Props to be passed to the content component. */
|
|
20
|
+
contentProps?: Record<string, any>;
|
|
21
|
+
/** Orientation of the scroller - horizontal (default) or vertical. */
|
|
22
|
+
orientation?: 'horizontal' | 'vertical';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const [Scroller, scroller] = hoistCmp.withFactory<ScrollerProps>({
|
|
26
|
+
displayName: 'Scroller',
|
|
27
|
+
model: creates(ScrollerModel, {publishMode: 'limited'}),
|
|
28
|
+
render({className, content, contentProps, model, orientation, ...layoutProps}) {
|
|
29
|
+
const {contentRef, isHorizontal} = model,
|
|
30
|
+
container = isHorizontal ? hbox : vbox;
|
|
31
|
+
return container({
|
|
32
|
+
...layoutProps,
|
|
33
|
+
className,
|
|
34
|
+
items: [
|
|
35
|
+
scrollButton({direction: 'backward', model}),
|
|
36
|
+
content({
|
|
37
|
+
...contentProps,
|
|
38
|
+
ref: composeRefs(
|
|
39
|
+
contentRef,
|
|
40
|
+
useOnResize(() => model.onViewportEvent())
|
|
41
|
+
)
|
|
42
|
+
}),
|
|
43
|
+
scrollButton({direction: 'forward', model})
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
interface ScrollButtonProps extends HoistProps<ScrollerModel> {
|
|
50
|
+
direction: 'forward' | 'backward';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const scrollButton = hoistCmp.factory<ScrollButtonProps>(({direction, model}) => {
|
|
54
|
+
if (!model.showScrollButtons) return null;
|
|
55
|
+
return button({
|
|
56
|
+
icon:
|
|
57
|
+
direction === 'backward'
|
|
58
|
+
? model.isHorizontal
|
|
59
|
+
? Icon.chevronLeft()
|
|
60
|
+
: Icon.chevronUp()
|
|
61
|
+
: model.isHorizontal
|
|
62
|
+
? Icon.chevronRight()
|
|
63
|
+
: Icon.chevronDown(),
|
|
64
|
+
disabled: direction === 'backward' ? model.isScrolledToStart : model.isScrolledToEnd,
|
|
65
|
+
onMouseDown: () => model.scroll(direction),
|
|
66
|
+
onMouseUp: () => model.stopScrolling(),
|
|
67
|
+
onMouseLeave: () => model.stopScrolling()
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {HoistModel} from '@xh/hoist/core';
|
|
2
|
+
import {makeObservable} from '@xh/hoist/mobx';
|
|
3
|
+
import {isNil} from 'lodash';
|
|
4
|
+
import {action, computed, observable} from 'mobx';
|
|
5
|
+
import {createRef} from 'react';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal model for the Scroller component. Used to manage the scroll state and provide
|
|
9
|
+
* scroll functionality. Uses animation frames to ensure smooth scrolling.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class ScrollerModel extends HoistModel {
|
|
14
|
+
contentRef = createRef<HTMLDivElement>();
|
|
15
|
+
|
|
16
|
+
@observable private scrollStart: number;
|
|
17
|
+
@observable private scrollSize: number;
|
|
18
|
+
@observable private clientSize: number;
|
|
19
|
+
|
|
20
|
+
private animationFrameId: number;
|
|
21
|
+
|
|
22
|
+
@computed
|
|
23
|
+
get showScrollButtons(): boolean {
|
|
24
|
+
return this.scrollSize > this.clientSize;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@computed
|
|
28
|
+
get isScrolledToStart(): boolean {
|
|
29
|
+
return this.scrollStart === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@computed
|
|
33
|
+
get isScrolledToEnd(): boolean {
|
|
34
|
+
// Allow for a 1px buffer to account for rounding errors
|
|
35
|
+
return this.scrollStart + this.clientSize >= this.scrollSize - 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get isHorizontal(): boolean {
|
|
39
|
+
return this.componentProps.orientation !== 'vertical';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
super();
|
|
44
|
+
makeObservable(this);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override afterLinked() {
|
|
48
|
+
this.contentRef.current.addEventListener('scroll', () => this.onViewportEvent());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
scroll(direction: 'forward' | 'backward') {
|
|
52
|
+
this.stopScrolling();
|
|
53
|
+
this.animationFrameId = window.requestAnimationFrame(() => {
|
|
54
|
+
const {current} = this.contentRef;
|
|
55
|
+
if (
|
|
56
|
+
!current ||
|
|
57
|
+
(direction === 'backward' && this.isScrolledToStart) ||
|
|
58
|
+
(direction === 'forward' && this.isScrolledToEnd)
|
|
59
|
+
) {
|
|
60
|
+
this.stopScrolling();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (this.isHorizontal) {
|
|
64
|
+
current.scrollLeft += direction === 'backward' ? -10 : 10;
|
|
65
|
+
} else {
|
|
66
|
+
current.scrollTop += direction === 'backward' ? -10 : 10;
|
|
67
|
+
}
|
|
68
|
+
this.scroll(direction);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stopScrolling() {
|
|
73
|
+
if (!isNil(this.animationFrameId)) {
|
|
74
|
+
window.cancelAnimationFrame(this.animationFrameId);
|
|
75
|
+
this.animationFrameId = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@action
|
|
80
|
+
onViewportEvent() {
|
|
81
|
+
const {contentRef, isHorizontal} = this,
|
|
82
|
+
{current} = contentRef;
|
|
83
|
+
this.scrollStart = isHorizontal ? current.scrollLeft : current.scrollTop;
|
|
84
|
+
this.scrollSize = isHorizontal ? current.scrollWidth : current.scrollHeight;
|
|
85
|
+
this.clientSize = isHorizontal ? current.clientWidth : current.clientHeight;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override destroy() {
|
|
89
|
+
this.stopScrolling();
|
|
90
|
+
super.destroy();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -5,9 +5,17 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {frame} from '@xh/hoist/cmp/layout';
|
|
8
|
-
import {TabModel} from '@xh/hoist/cmp/tab';
|
|
9
|
-
import {
|
|
8
|
+
import {TabContainerProps, TabModel} from '@xh/hoist/cmp/tab';
|
|
9
|
+
import {
|
|
10
|
+
hoistCmp,
|
|
11
|
+
HoistProps,
|
|
12
|
+
PlainObject,
|
|
13
|
+
refreshContextView,
|
|
14
|
+
TestSupportProps,
|
|
15
|
+
uses
|
|
16
|
+
} from '@xh/hoist/core';
|
|
10
17
|
import {elementFromContent} from '@xh/hoist/utils/react';
|
|
18
|
+
import {isFunction} from 'lodash';
|
|
11
19
|
import {useRef} from 'react';
|
|
12
20
|
import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
|
|
13
21
|
|
|
@@ -21,13 +29,17 @@ import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
|
|
|
21
29
|
*
|
|
22
30
|
* @internal
|
|
23
31
|
*/
|
|
24
|
-
|
|
32
|
+
interface TabProps extends HoistProps<TabModel>, TestSupportProps {
|
|
33
|
+
childContainerProps?: TabContainerProps['childContainerProps'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const tab = hoistCmp.factory<TabProps>({
|
|
25
37
|
displayName: 'Tab',
|
|
26
38
|
className: 'xh-tab',
|
|
27
39
|
model: uses(TabModel, {publishMode: 'limited'}),
|
|
28
40
|
|
|
29
|
-
render({model, className, testId}) {
|
|
30
|
-
const {content, isActive, renderMode, refreshContextModel} = model,
|
|
41
|
+
render({model, childContainerProps, className, testId}) {
|
|
42
|
+
const {childContainerModel, content, isActive, id, renderMode, refreshContextModel} = model,
|
|
31
43
|
wasActivated = useRef(false);
|
|
32
44
|
|
|
33
45
|
if (!wasActivated.current && isActive) wasActivated.current = true;
|
|
@@ -39,13 +51,25 @@ export const tab = hoistCmp.factory({
|
|
|
39
51
|
return null;
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
let contentProps: PlainObject = {flex: 1};
|
|
55
|
+
if (childContainerModel) {
|
|
56
|
+
if (isFunction(childContainerProps)) {
|
|
57
|
+
contentProps = {
|
|
58
|
+
...contentProps,
|
|
59
|
+
...childContainerProps({tabId: id, depth: childContainerModel.depth})
|
|
60
|
+
};
|
|
61
|
+
} else if (childContainerProps) {
|
|
62
|
+
contentProps = {...contentProps, ...childContainerProps};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
return frame({
|
|
43
67
|
display: isActive ? 'flex' : 'none',
|
|
44
68
|
className,
|
|
45
69
|
testId,
|
|
46
70
|
item: refreshContextView({
|
|
47
71
|
model: refreshContextModel,
|
|
48
|
-
item: errorBoundary(elementFromContent(content,
|
|
72
|
+
item: errorBoundary(elementFromContent(content, contentProps))
|
|
49
73
|
})
|
|
50
74
|
});
|
|
51
75
|
}
|
|
@@ -6,20 +6,29 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {div, hbox, placeholder, vbox} from '@xh/hoist/cmp/layout';
|
|
8
8
|
import {TabContainerModel, TabContainerProps} from '@xh/hoist/cmp/tab';
|
|
9
|
+
import {TabSwitcherProps} from '@xh/hoist/cmp/tab/Types';
|
|
9
10
|
import {getTestId} from '@xh/hoist/utils/js';
|
|
10
11
|
import {getLayoutProps} from '@xh/hoist/utils/react';
|
|
11
|
-
import {isEmpty} from 'lodash';
|
|
12
|
+
import {isEmpty, isNull, isObject} from 'lodash';
|
|
12
13
|
import '../Tabs.scss';
|
|
13
14
|
import {tabSwitcher} from '../TabSwitcher';
|
|
15
|
+
import {dynamicTabSwitcher} from '../dynamic/DynamicTabSwitcher';
|
|
14
16
|
import {tab} from './Tab';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Desktop implementation of TabContainer.
|
|
18
20
|
* @internal
|
|
19
21
|
*/
|
|
20
|
-
export function tabContainerImpl({
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
export function tabContainerImpl({
|
|
23
|
+
model,
|
|
24
|
+
childContainerProps,
|
|
25
|
+
className,
|
|
26
|
+
testId,
|
|
27
|
+
...props
|
|
28
|
+
}: TabContainerProps) {
|
|
29
|
+
const switcherProps = getSwitcherProps(props),
|
|
30
|
+
layoutProps = getLayoutProps(props),
|
|
31
|
+
vertical = ['left', 'right'].includes(switcherProps?.orientation),
|
|
23
32
|
container = vertical ? hbox : vbox;
|
|
24
33
|
|
|
25
34
|
// Default flex = 'auto' if no dimensions / flex specified.
|
|
@@ -31,21 +40,33 @@ export function tabContainerImpl({model, className, testId, ...props}: TabContai
|
|
|
31
40
|
...layoutProps,
|
|
32
41
|
className,
|
|
33
42
|
testId,
|
|
34
|
-
item: getChildren(model, testId)
|
|
43
|
+
item: getChildren(model, switcherProps, testId, childContainerProps)
|
|
35
44
|
});
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
function
|
|
47
|
+
function getSwitcherProps(tabContainerProps: TabContainerProps): TabSwitcherProps {
|
|
48
|
+
const {switcher} = tabContainerProps;
|
|
49
|
+
if (isObject(switcher)) return switcher;
|
|
50
|
+
return switcher === false || isNull(switcher) ? null : {orientation: 'top'};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getChildren(
|
|
54
|
+
model: TabContainerModel,
|
|
55
|
+
switcher: TabSwitcherProps,
|
|
56
|
+
testId: string,
|
|
57
|
+
childContainerProps: TabContainerProps['childContainerProps']
|
|
58
|
+
) {
|
|
39
59
|
const {tabs} = model;
|
|
40
60
|
if (isEmpty(tabs)) {
|
|
41
61
|
return div({className: 'xh-tab-wrapper', item: placeholder(model.emptyText)});
|
|
42
62
|
}
|
|
43
63
|
|
|
44
|
-
const {activeTabId,
|
|
64
|
+
const {activeTabId, dynamicTabSwitcherModel} = model,
|
|
45
65
|
switcherBefore = ['left', 'top'].includes(switcher?.orientation),
|
|
46
66
|
switcherAfter = ['right', 'bottom'].includes(switcher?.orientation),
|
|
67
|
+
switcherImpl = dynamicTabSwitcherModel ? dynamicTabSwitcher : tabSwitcher,
|
|
47
68
|
switcherCmp = switcher
|
|
48
|
-
?
|
|
69
|
+
? switcherImpl({key: 'switcher', testId: getTestId(testId, 'switcher'), ...switcher})
|
|
49
70
|
: null;
|
|
50
71
|
|
|
51
72
|
return [
|
|
@@ -58,7 +79,11 @@ function getChildren(model: TabContainerModel, testId: string) {
|
|
|
58
79
|
className: 'xh-tab-wrapper',
|
|
59
80
|
style,
|
|
60
81
|
key: tabId,
|
|
61
|
-
item: tab({
|
|
82
|
+
item: tab({
|
|
83
|
+
childContainerProps,
|
|
84
|
+
model: tabModel,
|
|
85
|
+
testId: getTestId(testId, tabId)
|
|
86
|
+
})
|
|
62
87
|
});
|
|
63
88
|
}),
|
|
64
89
|
switcherAfter ? switcherCmp : null
|
|
@@ -0,0 +1,21 @@
|
|
|
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 © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {TabSwitcherMenuContext} from '@xh/hoist/cmp/tab';
|
|
8
|
+
import {isMenuItem, type MenuItemLike, MenuToken} from '@xh/hoist/core';
|
|
9
|
+
|
|
10
|
+
/** @internal */
|
|
11
|
+
export function getContextMenuItem(
|
|
12
|
+
item: MenuItemLike<MenuToken, TabSwitcherMenuContext>,
|
|
13
|
+
context: TabSwitcherMenuContext
|
|
14
|
+
): MenuItemLike<MenuToken, TabSwitcherMenuContext> {
|
|
15
|
+
if (!isMenuItem(item)) return item;
|
|
16
|
+
const ret = {...item};
|
|
17
|
+
if (item.actionFn) ret.actionFn = e => item.actionFn(e, context);
|
|
18
|
+
if (item.prepareFn) ret.prepareFn = e => item.prepareFn(e, context);
|
|
19
|
+
if (item.items) ret.items = item.items.map(it => this.buildMenuItem(it, context));
|
|
20
|
+
return ret;
|
|
21
|
+
}
|
package/desktop/cmp/tab/index.ts
CHANGED
package/dynamics/desktop.ts
CHANGED
|
@@ -29,6 +29,7 @@ export let tabContainerImpl = null;
|
|
|
29
29
|
export let useContextMenu = null;
|
|
30
30
|
export let errorMessageImpl = null;
|
|
31
31
|
export let maskImpl = null;
|
|
32
|
+
export let DynamicTabSwitcherModel = null;
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Provide implementations of functions and classes exported in this file.
|
|
@@ -50,4 +51,5 @@ export function installDesktopImpls(impls) {
|
|
|
50
51
|
useContextMenu = impls.useContextMenu;
|
|
51
52
|
errorMessageImpl = impls.errorMessageImpl;
|
|
52
53
|
maskImpl = impls.maskImpl;
|
|
54
|
+
DynamicTabSwitcherModel = impls.DynamicTabSwitcherModel;
|
|
53
55
|
}
|
|
@@ -29,7 +29,7 @@ import {logWarn} from '@xh/hoist/utils/js';
|
|
|
29
29
|
|
|
30
30
|
export interface PanelProps extends HoistProps, Omit<BoxProps, 'title'> {
|
|
31
31
|
/** A toolbar to be docked at the bottom of the panel. */
|
|
32
|
-
bbar?:
|
|
32
|
+
bbar?: ReactNode;
|
|
33
33
|
|
|
34
34
|
/** CSS class name specific to the panel's header. */
|
|
35
35
|
headerClassName?: string;
|
|
@@ -62,7 +62,7 @@ export interface PanelProps extends HoistProps, Omit<BoxProps, 'title'> {
|
|
|
62
62
|
scrollable?: boolean;
|
|
63
63
|
|
|
64
64
|
/** A toolbar to be docked at the top of the panel. */
|
|
65
|
-
tbar?:
|
|
65
|
+
tbar?: ReactNode;
|
|
66
66
|
|
|
67
67
|
/** Title text added to the panel's header. */
|
|
68
68
|
title?: ReactNode;
|
|
@@ -10,23 +10,24 @@ import {page, tab as onsenTab, tabbar as onsenTabbar} from '@xh/hoist/kit/onsen'
|
|
|
10
10
|
import '@xh/hoist/mobile/register';
|
|
11
11
|
import {debounced, throwIf} from '@xh/hoist/utils/js';
|
|
12
12
|
import classNames from 'classnames';
|
|
13
|
-
import {isEmpty} from 'lodash';
|
|
13
|
+
import {isEmpty, isNull, isObject} from 'lodash';
|
|
14
14
|
import {tab} from './Tab';
|
|
15
15
|
import './Tabs.scss';
|
|
16
|
-
import {TabContainerProps, TabModel} from '@xh/hoist/cmp/tab';
|
|
16
|
+
import {TabContainerProps, TabModel, TabSwitcherProps} from '@xh/hoist/cmp/tab';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Mobile Implementation of TabContainer.
|
|
20
20
|
*
|
|
21
21
|
* @internal
|
|
22
22
|
*/
|
|
23
|
-
export function tabContainerImpl({model, className}: TabContainerProps) {
|
|
24
|
-
const
|
|
23
|
+
export function tabContainerImpl({model, className, ...props}: TabContainerProps) {
|
|
24
|
+
const switcherProps = getSwitcherProps(props),
|
|
25
|
+
{activeTab} = model,
|
|
25
26
|
tabs = model.tabs.filter(it => !it.excludeFromSwitcher),
|
|
26
27
|
impl = useLocalModel(TabContainerLocalModel);
|
|
27
28
|
|
|
28
29
|
throwIf(
|
|
29
|
-
|
|
30
|
+
switcherProps && !['top', 'bottom'].includes(switcherProps.orientation),
|
|
30
31
|
"Mobile TabContainer tab switcher orientation must be 'top', or 'bottom'"
|
|
31
32
|
);
|
|
32
33
|
|
|
@@ -38,19 +39,25 @@ export function tabContainerImpl({model, className}: TabContainerProps) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
return onsenTabbar({
|
|
41
|
-
className: classNames(className, `xh-tab-container--${
|
|
42
|
-
position:
|
|
42
|
+
className: classNames(className, `xh-tab-container--${switcherProps?.orientation}`),
|
|
43
|
+
position: switcherProps?.orientation,
|
|
43
44
|
activeIndex: activeTab ? tabs.indexOf(activeTab) : 0,
|
|
44
45
|
renderTabs: (idx, ref) => {
|
|
45
46
|
impl.setSwiper(ref);
|
|
46
47
|
return tabs.map(renderTabModel);
|
|
47
48
|
},
|
|
48
49
|
onPreChange: e => model.activateTab(tabs[e.index].id),
|
|
49
|
-
hideTabs: !
|
|
50
|
-
...
|
|
50
|
+
hideTabs: !switcherProps,
|
|
51
|
+
...switcherProps
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function getSwitcherProps(tabContainerProps: TabContainerProps): TabSwitcherProps {
|
|
56
|
+
const {switcher} = tabContainerProps;
|
|
57
|
+
if (isObject(switcher)) return switcher;
|
|
58
|
+
return switcher === false || isNull(switcher) ? null : {orientation: 'bottom'};
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
function renderTabModel(tabModel: TabModel) {
|
|
55
62
|
const {id, title, icon} = tabModel;
|
|
56
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "79.0.0-SNAPSHOT.
|
|
3
|
+
"version": "79.0.0-SNAPSHOT.1766097863558",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": "github:xh/hoist-react",
|
|
6
6
|
"homepage": "https://xh.io",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"react-beautiful-dnd": "~13.1.0",
|
|
68
68
|
"react-dates": "~21.8.0",
|
|
69
69
|
"react-dropzone": "~10.2.2",
|
|
70
|
-
"react-grid-layout": "1.
|
|
70
|
+
"react-grid-layout": "2.1.0",
|
|
71
71
|
"react-markdown": "~10.1.0",
|
|
72
72
|
"react-onsenui": "~1.13.2",
|
|
73
73
|
"react-popper": "~2.3.0",
|
|
@@ -93,7 +93,6 @@
|
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@types/react": "18.x",
|
|
95
95
|
"@types/react-dom": "18.x",
|
|
96
|
-
"@types/react-grid-layout": "1.3.5",
|
|
97
96
|
"@xh/hoist-dev-utils": "11.x",
|
|
98
97
|
"ag-grid-community": "34.x",
|
|
99
98
|
"ag-grid-react": "34.x",
|