@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1

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 (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { act, fireEvent, render, screen } from '@testing-library/react';
11
+ import { Tabs } from 'antd';
12
+ import React from 'react';
13
+ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
14
+
15
+ let IsolatedPageTabBar: (typeof import('../PageModelTabBar'))['FilteredPageTabBar'];
16
+
17
+ function StatefulContent() {
18
+ const [value, setValue] = React.useState('Initial value');
19
+ return <input aria-label="Tab draft" value={value} onChange={(event) => setValue(event.target.value)} />;
20
+ }
21
+
22
+ describe('FilteredPageTabBar module isolation', () => {
23
+ beforeAll(async () => {
24
+ vi.doMock('rc-tabs/es/TabContext', () => ({ default: React.createContext(undefined) }));
25
+ vi.doMock('rc-tabs/lib/TabContext', () => ({ default: React.createContext(undefined) }));
26
+ ({ FilteredPageTabBar: IsolatedPageTabBar } = await import('../PageModelTabBar'));
27
+ });
28
+
29
+ afterAll(() => {
30
+ vi.doUnmock('rc-tabs/es/TabContext');
31
+ vi.doUnmock('rc-tabs/lib/TabContext');
32
+ });
33
+
34
+ it('keeps filtering, focus, and aria behavior independent from direct rc-tabs contexts', () => {
35
+ const onChange = vi.fn();
36
+ const items = [
37
+ { key: 'tab-visible', label: 'Visible tab', children: 'Visible content' },
38
+ { key: 'tab-hidden', label: 'Hidden tab', children: 'Hidden content' },
39
+ ];
40
+
41
+ render(
42
+ <Tabs
43
+ activeKey="tab-hidden"
44
+ items={items}
45
+ onChange={onChange}
46
+ renderTabBar={(tabBarProps) => (
47
+ <IsolatedPageTabBar
48
+ hiddenTabKeys={new Set(['tab-hidden'])}
49
+ hiddenActiveTabLabel="Hidden tab"
50
+ items={items}
51
+ tabBarProps={tabBarProps}
52
+ />
53
+ )}
54
+ />,
55
+ );
56
+
57
+ const visibleTab = screen.getByRole('tab', { name: 'Visible tab' });
58
+ expect(visibleTab).toHaveAttribute('tabindex', '0');
59
+ expect(visibleTab).toHaveAttribute('aria-selected', 'false');
60
+ expect(screen.queryByRole('tab', { name: 'Hidden tab' })).not.toBeInTheDocument();
61
+ expect(screen.getByText('Hidden content')).toBeVisible();
62
+
63
+ const activePanel = screen.getByRole('tabpanel', { name: 'Hidden tab' });
64
+ expect(document.getElementById(activePanel.getAttribute('aria-labelledby') || '')).not.toBeNull();
65
+
66
+ fireEvent.focus(visibleTab);
67
+ fireEvent.keyDown(visibleTab, { code: 'Enter' });
68
+ expect(onChange).toHaveBeenCalledWith('tab-visible');
69
+ });
70
+
71
+ it('keeps the hidden active pane mounted across the isolated navigation rerender', () => {
72
+ const items = [
73
+ { key: 'tab-visible', label: 'Visible tab', children: 'Visible content' },
74
+ { key: 'tab-hidden', label: 'Hidden tab', children: <StatefulContent /> },
75
+ ];
76
+ const renderTabs = (hiddenTabKeys: Set<string>) => (
77
+ <Tabs
78
+ activeKey="tab-hidden"
79
+ items={items}
80
+ renderTabBar={
81
+ hiddenTabKeys.size > 0
82
+ ? (tabBarProps) => (
83
+ <IsolatedPageTabBar
84
+ hiddenTabKeys={hiddenTabKeys}
85
+ hiddenActiveTabLabel="Hidden tab"
86
+ items={items}
87
+ tabBarProps={tabBarProps}
88
+ />
89
+ )
90
+ : undefined
91
+ }
92
+ />
93
+ );
94
+ const { rerender } = render(renderTabs(new Set()));
95
+
96
+ fireEvent.change(screen.getByRole('textbox', { name: 'Tab draft' }), { target: { value: 'Unsaved value' } });
97
+ rerender(renderTabs(new Set(['tab-hidden'])));
98
+
99
+ expect(screen.queryByRole('tab', { name: 'Hidden tab' })).not.toBeInTheDocument();
100
+ expect(screen.getByRole('textbox', { name: 'Tab draft' })).toHaveValue('Unsaved value');
101
+ });
102
+
103
+ it('keeps hidden entries out of the keyboard roving model', async () => {
104
+ const items = [
105
+ { key: 'tab-a', label: 'Visible A', children: 'Content A' },
106
+ { key: 'tab-hidden', label: 'Hidden tab', children: 'Hidden content' },
107
+ { key: 'tab-b', label: 'Visible B', children: 'Content B' },
108
+ ];
109
+
110
+ render(
111
+ <Tabs
112
+ activeKey="tab-a"
113
+ items={items}
114
+ renderTabBar={(tabBarProps) => (
115
+ <IsolatedPageTabBar hiddenTabKeys={new Set(['tab-hidden'])} items={items} tabBarProps={tabBarProps} />
116
+ )}
117
+ />,
118
+ );
119
+
120
+ const firstTab = screen.getByRole('tab', { name: 'Visible A' });
121
+ const secondTab = screen.getByRole('tab', { name: 'Visible B' });
122
+ await act(async () => {
123
+ firstTab.focus();
124
+ fireEvent.keyDown(firstTab, { code: 'ArrowRight' });
125
+ });
126
+
127
+ expect(screen.queryByRole('tab', { name: 'Hidden tab' })).not.toBeInTheDocument();
128
+ expect(document.activeElement).toBe(secondTab);
129
+ });
130
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { fireEvent, render, screen } from '@testing-library/react';
11
+ import { Tabs } from 'antd';
12
+ import React from 'react';
13
+ import { describe, expect, it, vi } from 'vitest';
14
+ import { FilteredPageTabBar } from '../PageModelTabBar';
15
+
16
+ function StatefulContent() {
17
+ const [value, setValue] = React.useState('Initial value');
18
+ return <input aria-label="Tab draft" value={value} onChange={(event) => setValue(event.target.value)} />;
19
+ }
20
+
21
+ function renderTabs(hiddenTabKeys: Set<string>, onChange?: (activeKey: string) => void, activeKey = 'tab-hidden') {
22
+ const items = [
23
+ { key: 'tab-visible', label: 'Visible tab', children: 'Visible content' },
24
+ { key: 'tab-hidden', label: 'Hidden tab', children: <StatefulContent /> },
25
+ ];
26
+ return (
27
+ <Tabs
28
+ activeKey={activeKey}
29
+ onChange={onChange}
30
+ items={items}
31
+ renderTabBar={
32
+ hiddenTabKeys.size > 0
33
+ ? (tabBarProps) => (
34
+ <FilteredPageTabBar
35
+ hiddenTabKeys={hiddenTabKeys}
36
+ hiddenActiveTabLabel="Hidden tab"
37
+ items={items}
38
+ tabBarProps={tabBarProps}
39
+ />
40
+ )
41
+ : undefined
42
+ }
43
+ />
44
+ );
45
+ }
46
+
47
+ describe('FilteredPageTabBar', () => {
48
+ it('removes hidden tabs from the real navigation model while retaining their active pane', () => {
49
+ const hiddenTabKeys = new Set(['tab-hidden']);
50
+
51
+ render(renderTabs(hiddenTabKeys));
52
+
53
+ expect(screen.getByRole('tab', { name: 'Visible tab' })).toBeInTheDocument();
54
+ expect(screen.queryByRole('tab', { name: 'Hidden tab' })).not.toBeInTheDocument();
55
+ expect(screen.getByRole('textbox', { name: 'Tab draft' })).toBeVisible();
56
+ });
57
+
58
+ it('keeps the active pane mounted when its navigation entry becomes hidden', () => {
59
+ const { rerender } = render(renderTabs(new Set()));
60
+ const input = screen.getByRole('textbox', { name: 'Tab draft' });
61
+ fireEvent.change(input, { target: { value: 'Unsaved value' } });
62
+
63
+ rerender(renderTabs(new Set(['tab-hidden'])));
64
+
65
+ expect(screen.queryByRole('tab', { name: 'Hidden tab' })).not.toBeInTheDocument();
66
+ expect(screen.getByRole('textbox', { name: 'Tab draft' })).toHaveValue('Unsaved value');
67
+ });
68
+
69
+ it('keeps visible navigation focusable and gives the hidden active pane an accessible name', () => {
70
+ const onChange = vi.fn();
71
+ render(renderTabs(new Set(['tab-hidden']), onChange));
72
+
73
+ const visibleTab = screen.getByRole('tab', { name: 'Visible tab' });
74
+ expect(visibleTab).toHaveAttribute('tabindex', '0');
75
+ expect(visibleTab).toHaveAttribute('aria-selected', 'false');
76
+
77
+ const activePanel = screen.getByRole('tabpanel');
78
+ expect(activePanel).toHaveAccessibleName('Hidden tab');
79
+ expect(document.getElementById(activePanel.getAttribute('aria-labelledby') || '')).not.toBeNull();
80
+ expect(document.querySelectorAll('[role="tabpanel"]')).toHaveLength(1);
81
+
82
+ fireEvent.focus(visibleTab);
83
+ fireEvent.keyDown(visibleTab, { code: 'Enter' });
84
+ expect(onChange).toHaveBeenCalledWith('tab-visible');
85
+ });
86
+
87
+ it('connects a visible navigation entry to the pane owned by the outer Tabs instance', () => {
88
+ render(renderTabs(new Set(['tab-hidden']), undefined, 'tab-visible'));
89
+
90
+ const visibleTab = screen.getByRole('tab', { name: 'Visible tab' });
91
+ const activePanel = screen.getByRole('tabpanel', { name: 'Visible tab' });
92
+ expect(visibleTab).toHaveAttribute('aria-controls', activePanel.id);
93
+ expect(activePanel).toHaveAttribute('aria-labelledby', visibleTab.id);
94
+ expect(document.querySelectorAll(`#${CSS.escape(visibleTab.id)}`)).toHaveLength(1);
95
+ expect(document.querySelectorAll('[role="tabpanel"]')).toHaveLength(1);
96
+ });
97
+
98
+ it('does not duplicate panes when a tab item requests forceRender', () => {
99
+ const items = [
100
+ { key: 'tab-visible', label: 'Visible tab', children: 'Visible content', forceRender: true },
101
+ { key: 'tab-hidden', label: 'Hidden tab', children: 'Hidden content' },
102
+ ];
103
+
104
+ render(
105
+ <Tabs
106
+ activeKey="tab-visible"
107
+ items={items}
108
+ renderTabBar={(tabBarProps) => (
109
+ <FilteredPageTabBar hiddenTabKeys={new Set(['tab-hidden'])} items={items} tabBarProps={tabBarProps} />
110
+ )}
111
+ />,
112
+ );
113
+
114
+ const activePanel = screen.getByRole('tabpanel', { name: 'Visible tab' });
115
+ expect(document.querySelectorAll(`#${CSS.escape(activePanel.id)}`)).toHaveLength(1);
116
+ expect(document.querySelectorAll('[role="tabpanel"]')).toHaveLength(1);
117
+ });
118
+ });