@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/admin/AppComponent.ts +9 -1
  3. package/admin/AppModel.ts +0 -4
  4. package/admin/tabs/cluster/instances/InstancesTab.ts +1 -1
  5. package/admin/tabs/cluster/instances/InstancesTabModel.ts +0 -1
  6. package/admin/tabs/userData/roles/details/RoleDetailsModel.ts +0 -1
  7. package/build/types/cmp/tab/TabContainer.d.ts +19 -4
  8. package/build/types/cmp/tab/TabContainerModel.d.ts +18 -19
  9. package/build/types/cmp/tab/Types.d.ts +61 -0
  10. package/build/types/cmp/tab/index.d.ts +1 -1
  11. package/build/types/core/elem.d.ts +3 -3
  12. package/build/types/data/RecordAction.d.ts +4 -1
  13. package/build/types/desktop/cmp/dash/canvas/DashCanvas.d.ts +3 -2
  14. package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +45 -3
  15. package/build/types/desktop/cmp/panel/Panel.d.ts +2 -2
  16. package/build/types/desktop/cmp/rest/RestGrid.d.ts +3 -3
  17. package/build/types/desktop/cmp/tab/TabSwitcher.d.ts +1 -1
  18. package/build/types/desktop/cmp/tab/dynamic/DynamicTabSwitcher.d.ts +7 -0
  19. package/build/types/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel.d.ts +30 -0
  20. package/build/types/desktop/cmp/tab/dynamic/scroller/Scroller.d.ts +19 -0
  21. package/build/types/desktop/cmp/tab/dynamic/scroller/ScrollerModel.d.ts +23 -0
  22. package/build/types/desktop/cmp/tab/impl/Tab.d.ts +7 -2
  23. package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
  24. package/build/types/desktop/cmp/tab/impl/TabContextMenuItems.d.ts +4 -0
  25. package/build/types/desktop/cmp/tab/index.d.ts +1 -0
  26. package/build/types/dynamics/desktop.d.ts +1 -0
  27. package/build/types/mobile/cmp/panel/Panel.d.ts +2 -2
  28. package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
  29. package/cmp/tab/TabContainer.ts +19 -4
  30. package/cmp/tab/TabContainerModel.ts +113 -54
  31. package/cmp/tab/TabModel.ts +1 -2
  32. package/cmp/tab/Types.ts +80 -0
  33. package/cmp/tab/index.ts +1 -1
  34. package/core/elem.ts +5 -5
  35. package/data/RecordAction.ts +4 -1
  36. package/desktop/appcontainer/AppContainer.ts +3 -2
  37. package/desktop/cmp/dash/canvas/DashCanvas.ts +57 -35
  38. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +135 -21
  39. package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilter.ts +1 -1
  40. package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.ts +0 -1
  41. package/desktop/cmp/panel/Panel.ts +2 -2
  42. package/desktop/cmp/rest/RestGrid.ts +4 -5
  43. package/desktop/cmp/tab/TabSwitcher.ts +18 -3
  44. package/desktop/cmp/tab/Tabs.scss +1 -0
  45. package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.scss +53 -0
  46. package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.ts +237 -0
  47. package/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel.ts +167 -0
  48. package/desktop/cmp/tab/dynamic/scroller/Scroller.ts +69 -0
  49. package/desktop/cmp/tab/dynamic/scroller/ScrollerModel.ts +92 -0
  50. package/desktop/cmp/tab/impl/Tab.ts +30 -6
  51. package/desktop/cmp/tab/impl/TabContainer.ts +34 -9
  52. package/desktop/cmp/tab/impl/TabContextMenuItems.ts +21 -0
  53. package/desktop/cmp/tab/index.ts +1 -0
  54. package/dynamics/desktop.ts +2 -0
  55. package/mobile/cmp/panel/Panel.ts +2 -2
  56. package/mobile/cmp/tab/impl/TabContainer.ts +16 -9
  57. package/package.json +2 -3
  58. package/tsconfig.tsbuildinfo +1 -1
  59. package/build/types/cmp/tab/TabSwitcherProps.d.ts +0 -16
  60. 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 {hoistCmp, refreshContextView, uses} from '@xh/hoist/core';
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
- export const tab = hoistCmp.factory({
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, {flex: 1}))
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({model, className, testId, ...props}: TabContainerProps) {
21
- const layoutProps = getLayoutProps(props),
22
- vertical = ['left', 'right'].includes(model.switcher?.orientation),
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 getChildren(model: TabContainerModel, testId: string) {
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, switcher} = model,
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
- ? tabSwitcher({key: 'switcher', testId: getTestId(testId, 'switcher'), ...switcher})
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({model: tabModel, testId: getTestId(testId, tabId)})
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
+ }
@@ -5,3 +5,4 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  export * from './TabSwitcher';
8
+ export * from './dynamic/DynamicTabSwitcher';
@@ -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?: Some<ReactNode>;
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?: Some<ReactNode>;
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 {activeTab, switcher} = model,
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
- switcher && !['top', 'bottom'].includes(switcher.orientation),
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--${switcher?.orientation}`),
42
- position: switcher?.orientation,
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: !switcher,
50
- ...switcher
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.1766020485210",
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.5.0",
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",