dockview 6.6.0 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +33 -26
  2. package/dist/cjs/index.d.ts +0 -9
  3. package/dist/cjs/index.js +12 -7
  4. package/dist/dockview.js +20486 -914
  5. package/dist/dockview.min.js +16 -2
  6. package/dist/dockview.min.js.map +1 -1
  7. package/dist/esm/index.d.ts +0 -9
  8. package/dist/esm/index.js +12 -7
  9. package/dist/package/main.cjs.js +856 -941
  10. package/dist/package/main.cjs.min.js +8 -2
  11. package/dist/package/main.esm.min.mjs +8 -2
  12. package/dist/package/main.esm.mjs +854 -934
  13. package/dist/styles/dockview.css +275 -13
  14. package/package.json +16 -12
  15. package/dist/cjs/dockview/defaultTab.d.ts +0 -7
  16. package/dist/cjs/dockview/defaultTab.js +0 -99
  17. package/dist/cjs/dockview/dockview.d.ts +0 -33
  18. package/dist/cjs/dockview/dockview.js +0 -285
  19. package/dist/cjs/dockview/headerActionsRenderer.d.ts +0 -24
  20. package/dist/cjs/dockview/headerActionsRenderer.js +0 -86
  21. package/dist/cjs/dockview/reactContentPart.d.ts +0 -21
  22. package/dist/cjs/dockview/reactContentPart.js +0 -52
  23. package/dist/cjs/dockview/reactContextMenuItemPart.d.ts +0 -14
  24. package/dist/cjs/dockview/reactContextMenuItemPart.js +0 -31
  25. package/dist/cjs/dockview/reactGroupDragGhostPart.d.ts +0 -20
  26. package/dist/cjs/dockview/reactGroupDragGhostPart.js +0 -32
  27. package/dist/cjs/dockview/reactHeaderPart.d.ts +0 -17
  28. package/dist/cjs/dockview/reactHeaderPart.js +0 -46
  29. package/dist/cjs/dockview/reactTabGroupChipPart.d.ts +0 -23
  30. package/dist/cjs/dockview/reactTabGroupChipPart.js +0 -36
  31. package/dist/cjs/dockview/reactWatermarkPart.d.ts +0 -18
  32. package/dist/cjs/dockview/reactWatermarkPart.js +0 -47
  33. package/dist/cjs/gridview/gridview.d.ts +0 -17
  34. package/dist/cjs/gridview/gridview.js +0 -105
  35. package/dist/cjs/gridview/view.d.ts +0 -9
  36. package/dist/cjs/gridview/view.js +0 -41
  37. package/dist/cjs/paneview/paneview.d.ts +0 -20
  38. package/dist/cjs/paneview/paneview.js +0 -137
  39. package/dist/cjs/paneview/view.d.ts +0 -19
  40. package/dist/cjs/paneview/view.js +0 -44
  41. package/dist/cjs/react.d.ts +0 -35
  42. package/dist/cjs/react.js +0 -183
  43. package/dist/cjs/splitview/splitview.d.ts +0 -17
  44. package/dist/cjs/splitview/splitview.js +0 -105
  45. package/dist/cjs/splitview/view.d.ts +0 -9
  46. package/dist/cjs/splitview/view.js +0 -39
  47. package/dist/cjs/svg.d.ts +0 -3
  48. package/dist/cjs/svg.js +0 -15
  49. package/dist/cjs/types.d.ts +0 -4
  50. package/dist/cjs/types.js +0 -2
  51. package/dist/esm/dockview/defaultTab.d.ts +0 -7
  52. package/dist/esm/dockview/defaultTab.js +0 -65
  53. package/dist/esm/dockview/dockview.d.ts +0 -33
  54. package/dist/esm/dockview/dockview.js +0 -252
  55. package/dist/esm/dockview/headerActionsRenderer.d.ts +0 -24
  56. package/dist/esm/dockview/headerActionsRenderer.js +0 -72
  57. package/dist/esm/dockview/reactContentPart.d.ts +0 -21
  58. package/dist/esm/dockview/reactContentPart.js +0 -43
  59. package/dist/esm/dockview/reactContextMenuItemPart.d.ts +0 -14
  60. package/dist/esm/dockview/reactContextMenuItemPart.js +0 -22
  61. package/dist/esm/dockview/reactGroupDragGhostPart.d.ts +0 -20
  62. package/dist/esm/dockview/reactGroupDragGhostPart.js +0 -23
  63. package/dist/esm/dockview/reactHeaderPart.d.ts +0 -17
  64. package/dist/esm/dockview/reactHeaderPart.js +0 -37
  65. package/dist/esm/dockview/reactTabGroupChipPart.d.ts +0 -23
  66. package/dist/esm/dockview/reactTabGroupChipPart.js +0 -27
  67. package/dist/esm/dockview/reactWatermarkPart.d.ts +0 -18
  68. package/dist/esm/dockview/reactWatermarkPart.js +0 -38
  69. package/dist/esm/gridview/gridview.d.ts +0 -17
  70. package/dist/esm/gridview/gridview.js +0 -72
  71. package/dist/esm/gridview/view.d.ts +0 -9
  72. package/dist/esm/gridview/view.js +0 -19
  73. package/dist/esm/paneview/paneview.d.ts +0 -20
  74. package/dist/esm/paneview/paneview.js +0 -104
  75. package/dist/esm/paneview/view.d.ts +0 -19
  76. package/dist/esm/paneview/view.js +0 -35
  77. package/dist/esm/react.d.ts +0 -35
  78. package/dist/esm/react.js +0 -134
  79. package/dist/esm/splitview/splitview.d.ts +0 -17
  80. package/dist/esm/splitview/splitview.js +0 -72
  81. package/dist/esm/splitview/view.d.ts +0 -9
  82. package/dist/esm/splitview/view.js +0 -17
  83. package/dist/esm/svg.d.ts +0 -3
  84. package/dist/esm/svg.js +0 -7
  85. package/dist/esm/types.d.ts +0 -4
  86. package/dist/esm/types.js +0 -1
@@ -1,1033 +1,953 @@
1
1
  /**
2
2
  * dockview
3
- * @version 6.6.0
3
+ * @version 7.0.2
4
4
  * @link https://github.com/mathuo/dockview
5
5
  * @license MIT
6
6
  */
7
- import { DockviewDisposable, DockviewEmitter, DockviewMutableDisposable, DockviewCompositeDisposable, PROPERTY_KEYS_DOCKVIEW, createDockview, SplitviewPanel, SplitviewApi, PROPERTY_KEYS_SPLITVIEW, createSplitview, GridviewPanel, GridviewApi, PROPERTY_KEYS_GRIDVIEW, createGridview, PROPERTY_KEYS_PANEVIEW, createPaneview } from 'dockview-core';
7
+ import { defineModule, DockviewCompositeDisposable, DockviewDisposable, findRelativeZIndexParent, LiveRegionModule, resolveMessages, registerModules, markDockviewPackageLoaded } from 'dockview-core';
8
8
  export * from 'dockview-core';
9
- import React from 'react';
10
- import ReactDOM from 'react-dom';
11
9
 
12
10
  /**
13
- * This component is intended to interface between vanilla-js and React hence we need to be
14
- * creative in how we update props.
15
- * A ref of the component is exposed with an update method; which when called stores the props
16
- * as a ref within this component and forcefully triggers a re-render of the component using
17
- * the ref of props we just set on the renderered component as the props passed to the inner
18
- * component
19
- */
20
- const ReactComponentBridge = (props, ref) => {
21
- const [, triggerRender] = React.useState(0);
22
- const _props = React.useRef(props.componentProps);
23
- React.useImperativeHandle(ref, () => ({
24
- update: (componentProps) => {
25
- _props.current = Object.assign(Object.assign({}, _props.current), componentProps);
26
- /**
27
- * setting a arbitrary piece of state within this component will
28
- * trigger a re-render.
29
- * we use this rather than updating through a prop since we can
30
- * pass a ref into the vanilla-js world.
31
- *
32
- * Use a monotonic counter rather than `Date.now()` so two
33
- * updates within the same millisecond still produce distinct
34
- * state values and avoid React's bailout.
35
- */
36
- triggerRender((n) => n + 1);
37
- },
38
- }), []);
39
- return React.createElement(props.component, _props.current);
40
- };
41
- ReactComponentBridge.displayName = 'DockviewReactJsBridge';
42
- /**
43
- * Since we are storing the React.Portal references in a rendered array they
44
- * require a key property like any other React element rendered in an array
45
- * to prevent excessive re-rendering
11
+ * dockview-modules
12
+ * @version 7.0.2
13
+ * @link https://github.com/mathuo/dockview
14
+ * @license MIT
46
15
  */
47
- const uniquePortalKeyGenerator = (() => {
48
- let value = 1;
49
- return { next: () => `dockview_react_portal_key_${(value++).toString()}` };
50
- })();
51
- const ReactPartContext = React.createContext({});
52
- class ReactPart {
53
- constructor(parent, portalStore, component, parameters, context) {
54
- this.parent = parent;
55
- this.portalStore = portalStore;
56
- this.component = component;
57
- this.parameters = parameters;
58
- this.context = context;
59
- this._initialProps = {};
60
- this.disposed = false;
61
- this.createPortal();
16
+
17
+ class TabGroupChipsService {
18
+ constructor(host) {
19
+ this._host = host;
20
+ }
21
+ attachToGroup(group) {
22
+ return new DockviewCompositeDisposable(group.model.onDidCreateTabGroup((e) => {
23
+ this._host.fireDidCreateTabGroup(e);
24
+ }), group.model.onDidDestroyTabGroup((e) => {
25
+ this._host.fireDidDestroyTabGroup(e);
26
+ }), group.model.onDidAddPanelToTabGroup((e) => {
27
+ this._host.fireDidAddPanelToTabGroup(e);
28
+ }), group.model.onDidRemovePanelFromTabGroup((e) => {
29
+ this._host.fireDidRemovePanelFromTabGroup(e);
30
+ }), group.model.onDidTabGroupChange((e) => {
31
+ this._host.fireDidTabGroupChange(e);
32
+ }), group.model.onDidTabGroupCollapsedChange((e) => {
33
+ this._host.fireDidTabGroupCollapsedChange(e);
34
+ }));
62
35
  }
63
- update(props) {
64
- if (this.disposed) {
65
- throw new Error('invalid operation: resource is already disposed');
66
- }
67
- if (!this.componentInstance) {
68
- // if the component is yet to be mounted store the props
69
- this._initialProps = Object.assign(Object.assign({}, this._initialProps), props);
70
- }
71
- else {
72
- this.componentInstance.update(props);
73
- }
36
+ dispose() {
37
+ // No internal state to tear down — emitters live on the host.
74
38
  }
75
- createPortal() {
76
- if (this.disposed) {
77
- throw new Error('invalid operation: resource is already disposed');
78
- }
79
- if (!isReactComponent(this.component)) {
80
- /**
81
- * we know this isn't a React.FunctionComponent so throw an error here.
82
- * if we do not intercept then React library will throw a very obsure error
83
- * for the same reason... at least at this point we will emit a sensible stacktrace.
84
- */
85
- throw new Error('Dockview: Only React.memo(...), React.ForwardRef(...) and functional components are accepted as components');
86
- }
87
- const bridgeComponent = React.createElement(React.forwardRef(ReactComponentBridge), {
88
- component: this
89
- .component,
90
- componentProps: this.parameters,
91
- ref: (element) => {
92
- this.componentInstance = element;
93
- if (Object.keys(this._initialProps).length > 0) {
94
- this.componentInstance.update(this._initialProps);
95
- this._initialProps = {}; // don't keep a reference to the users object once no longer required
39
+ }
40
+ const TabGroupChipsModule = defineModule({
41
+ name: 'TabGroupChips',
42
+ serviceKey: 'tabGroupChipsService',
43
+ create: (host) => new TabGroupChipsService(host),
44
+ init: (host, service) => {
45
+ // Self-attach to existing and future groups; tear down when groups
46
+ // are removed. Component doesn't need to know about this wiring.
47
+ const perGroupDisposables = new Map();
48
+ return new DockviewCompositeDisposable(host.onDidAddGroup((group) => {
49
+ perGroupDisposables.set(group, service.attachToGroup(group));
50
+ }), host.onDidRemoveGroup((group) => {
51
+ var _a;
52
+ (_a = perGroupDisposables.get(group)) === null || _a === void 0 ? void 0 : _a.dispose();
53
+ perGroupDisposables.delete(group);
54
+ }), {
55
+ dispose: () => {
56
+ for (const d of perGroupDisposables.values()) {
57
+ d.dispose();
96
58
  }
59
+ perGroupDisposables.clear();
97
60
  },
98
61
  });
99
- const node = this.context
100
- ? React.createElement(ReactPartContext.Provider, { value: this.context }, bridgeComponent)
101
- : bridgeComponent;
102
- const portal = ReactDOM.createPortal(node, this.parent, uniquePortalKeyGenerator.next());
103
- this.ref = {
104
- portal,
105
- disposable: this.portalStore.addPortal(portal),
106
- };
107
- }
108
- dispose() {
109
- var _a;
110
- (_a = this.ref) === null || _a === void 0 ? void 0 : _a.disposable.dispose();
111
- this.disposed = true;
112
- }
62
+ },
63
+ });
64
+
65
+ function popoverZIndexFor(target) {
66
+ if (!(target instanceof HTMLElement)) {
67
+ return undefined;
68
+ }
69
+ // Floating overlays live in the shell as siblings of the popover anchor
70
+ // and the AriaLevelTracker sets their inline z-index. Without this, a
71
+ // popover opened from inside a floating group would render behind it
72
+ // because they share the shell stacking context.
73
+ const relativeParent = findRelativeZIndexParent(target);
74
+ return (relativeParent === null || relativeParent === void 0 ? void 0 : relativeParent.style.zIndex)
75
+ ? `calc(${relativeParent.style.zIndex} * 2)`
76
+ : undefined;
113
77
  }
114
- /**
115
- * A React Hook that returns an array of portals to be rendered by the user of this hook
116
- * and a disposable function to add a portal. Calling dispose removes this portal from the
117
- * portal array
118
- */
119
- const usePortalsLifecycle = () => {
120
- const [portals, setPortals] = React.useState([]);
121
- React.useDebugValue(`Portal count: ${portals.length}`);
122
- const addPortal = React.useCallback((portal) => {
123
- setPortals((existingPortals) => [...existingPortals, portal]);
124
- let disposed = false;
125
- return DockviewDisposable.from(() => {
126
- if (disposed) {
127
- throw new Error('invalid operation: resource already disposed');
128
- }
129
- disposed = true;
130
- setPortals((existingPortals) => existingPortals.filter((p) => p !== portal));
131
- });
132
- }, []);
133
- return [portals, addPortal];
134
- };
135
- function isReactComponent(component) {
136
- /**
137
- * Yes, we could use "react-is" but that would introduce an unwanted peer dependency
138
- * so for now we will check in a rather crude fashion...
139
- */
140
- return (typeof component === 'function' /** Functional Componnts */ ||
141
- !!(component === null || component === void 0 ? void 0 : component.$$typeof) /** React.memo(...) Components */);
78
+ let _nextId = 0;
79
+ const nextContextMenuItemId = () => `dv-ctx-menu-item-${_nextId++}`;
80
+ function isItemConfig(item) {
81
+ return typeof item === 'object';
142
82
  }
143
-
144
- class ReactPanelContentPart {
145
- get element() {
146
- return this._element;
147
- }
148
- constructor(id, component, reactPortalStore) {
149
- this.id = id;
150
- this.component = component;
151
- this.reactPortalStore = reactPortalStore;
152
- this._onDidFocus = new DockviewEmitter();
153
- this.onDidFocus = this._onDidFocus.event;
154
- this._onDidBlur = new DockviewEmitter();
155
- this.onDidBlur = this._onDidBlur.event;
156
- this._element = document.createElement('div');
157
- this._element.className = 'dv-react-part';
158
- this._element.style.height = '100%';
159
- this._element.style.width = '100%';
160
- }
161
- focus() {
162
- // TODO
163
- }
164
- init(parameters) {
165
- this.part = new ReactPart(this.element, this.reactPortalStore, this.component, {
166
- params: parameters.params,
167
- api: parameters.api,
168
- containerApi: parameters.containerApi,
169
- });
170
- }
171
- update(event) {
172
- var _a;
173
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.update({ params: event.params });
174
- }
175
- layout(_width, _height) {
176
- // noop
177
- }
178
- dispose() {
179
- var _a;
180
- this._onDidFocus.dispose();
181
- this._onDidBlur.dispose();
182
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.dispose();
183
- }
83
+ function buildItem(label, close, action, disabled) {
84
+ const el = document.createElement('div');
85
+ el.className = 'dv-context-menu-item';
86
+ el.setAttribute('role', 'menuitem');
87
+ if (disabled) {
88
+ el.classList.add('dv-context-menu-item--disabled');
89
+ el.setAttribute('aria-disabled', 'true');
90
+ }
91
+ el.textContent = label;
92
+ if (!disabled) {
93
+ el.addEventListener('click', () => {
94
+ action();
95
+ close();
96
+ });
97
+ }
98
+ return el;
184
99
  }
185
-
186
- class ReactPanelHeaderPart {
187
- get element() {
188
- return this._element;
189
- }
190
- constructor(id, component, reactPortalStore) {
191
- this.id = id;
192
- this.component = component;
193
- this.reactPortalStore = reactPortalStore;
194
- this._element = document.createElement('div');
195
- this._element.className = 'dv-react-part';
196
- this._element.style.height = '100%';
197
- this._element.style.width = '100%';
198
- }
199
- focus() {
200
- //noop
201
- }
202
- init(parameters) {
203
- this.part = new ReactPart(this.element, this.reactPortalStore, this.component, {
204
- params: parameters.params,
205
- api: parameters.api,
206
- containerApi: parameters.containerApi,
207
- tabLocation: parameters.tabLocation,
208
- });
209
- }
210
- update(event) {
211
- var _a;
212
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.update({ params: event.params });
213
- }
214
- layout(_width, _height) {
215
- // noop - retrieval from api
216
- }
217
- dispose() {
218
- var _a;
219
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.dispose();
220
- }
100
+ function buildSeparator() {
101
+ const el = document.createElement('div');
102
+ el.className = 'dv-context-menu-separator';
103
+ el.setAttribute('role', 'separator');
104
+ return el;
221
105
  }
222
-
223
- class ReactWatermarkPart {
224
- get element() {
225
- return this._element;
106
+ function isCoarsePrimaryInput() {
107
+ if (typeof window === 'undefined' || !window.matchMedia) {
108
+ return false;
226
109
  }
227
- constructor(id, component, reactPortalStore) {
228
- this.id = id;
229
- this.component = component;
230
- this.reactPortalStore = reactPortalStore;
231
- this._element = document.createElement('div');
232
- this._element.className = 'dv-react-part';
233
- this._element.style.height = '100%';
234
- this._element.style.width = '100%';
110
+ const coarse = window.matchMedia('(pointer: coarse)').matches;
111
+ const fine = window.matchMedia('(pointer: fine)').matches;
112
+ return coarse && !fine;
113
+ }
114
+ function buildRenameInput(tabGroup) {
115
+ const wrapper = document.createElement('div');
116
+ wrapper.className = 'dv-context-menu-rename';
117
+ const input = document.createElement('input');
118
+ input.className = 'dv-context-menu-rename-input';
119
+ input.type = 'text';
120
+ input.placeholder = 'Name This Group';
121
+ input.value = tabGroup.label;
122
+ input.addEventListener('input', () => {
123
+ tabGroup.setLabel(input.value);
124
+ });
125
+ input.addEventListener('keydown', (e) => {
126
+ if (e.key !== 'Escape' && e.key !== 'Enter') {
127
+ e.stopPropagation();
128
+ }
129
+ });
130
+ input.addEventListener('click', (e) => {
131
+ e.stopPropagation();
132
+ });
133
+ wrapper.appendChild(input);
134
+ // Skip auto-focus on touch-primary devices: focusing the input pops the
135
+ // on-screen keyboard, which fires `window resize`, which `PopupService`
136
+ // listens to and uses to dismiss the popover — so the menu opens, the
137
+ // keyboard appears, and the menu immediately closes before the user can
138
+ // type. The user can still tap the input to focus it intentionally.
139
+ if (!isCoarsePrimaryInput()) {
140
+ requestAnimationFrame(() => {
141
+ input.focus();
142
+ input.select();
143
+ });
144
+ }
145
+ return wrapper;
146
+ }
147
+ function buildColorPicker(tabGroup, palette) {
148
+ const wrapper = document.createElement('div');
149
+ wrapper.className = 'dv-context-menu-color-picker';
150
+ if (!palette.enabled) {
151
+ // Opt-out: render no swatches. Returning a wrapper rather than null
152
+ // keeps the call site simple; the wrapper is empty and visually inert.
153
+ return wrapper;
154
+ }
155
+ for (const entry of palette.entries()) {
156
+ const swatch = document.createElement('div');
157
+ swatch.className = 'dv-context-menu-color-swatch';
158
+ // Use a CSS custom property rather than setting `backgroundColor`
159
+ // directly: the IDL setter validates the value against a color
160
+ // grammar and rejects `var(...)` references in some environments
161
+ // (notably jsdom; some browsers have historically had similar
162
+ // quirks). The matching SCSS rule reads the var at use time.
163
+ swatch.style.setProperty('--dv-tab-group-color', entry.value);
164
+ if (entry.label) {
165
+ swatch.title = entry.label;
166
+ }
167
+ if (tabGroup.color === entry.id) {
168
+ swatch.classList.add('dv-context-menu-color-swatch--selected');
169
+ }
170
+ swatch.addEventListener('click', () => {
171
+ tabGroup.setColor(entry.id);
172
+ });
173
+ wrapper.appendChild(swatch);
174
+ }
175
+ return wrapper;
176
+ }
177
+ class ContextMenuController {
178
+ constructor(accessor) {
179
+ this.accessor = accessor;
235
180
  }
236
- init(parameters) {
237
- this.part = new ReactPart(this.element, this.reactPortalStore, this.component, {
238
- group: parameters.group,
239
- containerApi: parameters.containerApi,
181
+ show(panel, group, event) {
182
+ var _a, _b;
183
+ if (!this.accessor.options.getTabContextMenuItems) {
184
+ return;
185
+ }
186
+ const items = this.accessor.options.getTabContextMenuItems({
187
+ panel,
188
+ group,
189
+ api: this.accessor.api,
190
+ event,
240
191
  });
241
- }
242
- focus() {
243
- // noop
244
- }
245
- update(params) {
246
- var _a, _b, _c;
247
- if (this.parameters) {
248
- this.parameters.params = params.params;
192
+ if (items.length === 0) {
193
+ return;
249
194
  }
250
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.update({ params: (_c = (_b = this.parameters) === null || _b === void 0 ? void 0 : _b.params) !== null && _c !== void 0 ? _c : {} });
251
- }
252
- layout(_width, _height) {
253
- // noop - retrieval from api
254
- }
255
- dispose() {
256
- var _a;
257
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.dispose();
258
- }
259
- }
260
-
261
- class ReactHeaderActionsRendererPart {
262
- get element() {
263
- return this._element;
264
- }
265
- get part() {
266
- return this._part;
267
- }
268
- constructor(component, reactPortalStore, _group) {
269
- this.component = component;
270
- this.reactPortalStore = reactPortalStore;
271
- this._group = _group;
272
- this.mutableDisposable = new DockviewMutableDisposable();
273
- this._element = document.createElement('div');
274
- this._element.className = 'dv-react-part';
275
- this._element.style.height = '100%';
276
- this._element.style.width = '100%';
277
- }
278
- init(parameters) {
279
- this.mutableDisposable.value = new DockviewCompositeDisposable(this._group.model.onDidAddPanel(() => {
280
- this.updatePanels();
281
- }), this._group.model.onDidRemovePanel(() => {
282
- this.updatePanels();
283
- }), this._group.model.onDidActivePanelChange(() => {
284
- this.updateActivePanel();
285
- }), parameters.api.onDidActiveChange(() => {
286
- this.updateGroupActive();
287
- }), parameters.api.onDidLocationChange((event) => {
288
- this.updateLocation(event.location);
289
- }));
290
- this._part = new ReactPart(this.element, this.reactPortalStore, this.component, {
291
- api: parameters.api,
292
- containerApi: parameters.containerApi,
293
- panels: this._group.model.panels,
294
- activePanel: this._group.model.activePanel,
295
- isGroupActive: this._group.api.isActive,
296
- group: this._group,
297
- headerPosition: this._group.model.headerPosition,
298
- location: parameters.api.location,
195
+ event.preventDefault();
196
+ const popupService = this.accessor.getPopupServiceForGroup(group);
197
+ const close = () => popupService.close();
198
+ const menuEl = document.createElement('div');
199
+ menuEl.className = 'dv-context-menu';
200
+ menuEl.setAttribute('role', 'menu');
201
+ for (const item of items) {
202
+ if (item === 'separator') {
203
+ menuEl.appendChild(buildSeparator());
204
+ }
205
+ else if (item === 'close') {
206
+ menuEl.appendChild(buildItem('Close', close, () => panel.api.close()));
207
+ }
208
+ else if (item === 'closeOthers') {
209
+ menuEl.appendChild(buildItem('Close Others', close, () => {
210
+ group.panels
211
+ .filter((p) => p !== panel)
212
+ .forEach((p) => p.api.close());
213
+ }));
214
+ }
215
+ else if (item === 'closeAll') {
216
+ menuEl.appendChild(buildItem('Close All', close, () => {
217
+ [...group.panels].forEach((p) => p.api.close());
218
+ }));
219
+ }
220
+ else if (isItemConfig(item) && item.element) {
221
+ menuEl.appendChild(item.element);
222
+ }
223
+ else if (isItemConfig(item) && item.component) {
224
+ const renderer = (_b = (_a = this.accessor.options).createContextMenuItemComponent) === null || _b === void 0 ? void 0 : _b.call(_a, {
225
+ id: nextContextMenuItemId(),
226
+ component: item.component,
227
+ });
228
+ if (renderer) {
229
+ renderer.init({
230
+ panel,
231
+ group,
232
+ api: this.accessor.api,
233
+ close,
234
+ componentProps: item.componentProps,
235
+ });
236
+ menuEl.appendChild(renderer.element);
237
+ }
238
+ }
239
+ else if (isItemConfig(item) && item.label) {
240
+ menuEl.appendChild(buildItem(item.label, close, () => { var _a; return (_a = item.action) === null || _a === void 0 ? void 0 : _a.call(item); }, item.disabled));
241
+ }
242
+ }
243
+ popupService.openPopover(menuEl, {
244
+ x: event.clientX,
245
+ y: event.clientY,
246
+ zIndex: popoverZIndexFor(event.target),
299
247
  });
300
248
  }
301
- dispose() {
302
- var _a;
303
- this.mutableDisposable.dispose();
304
- (_a = this._part) === null || _a === void 0 ? void 0 : _a.dispose();
305
- }
306
- update(event) {
307
- var _a;
308
- (_a = this._part) === null || _a === void 0 ? void 0 : _a.update(event.params);
309
- }
310
- updatePanels() {
311
- this.update({ params: { panels: this._group.model.panels } });
312
- }
313
- updateActivePanel() {
314
- this.update({
315
- params: {
316
- activePanel: this._group.model.activePanel,
317
- },
249
+ showForChip(tabGroup, group, event) {
250
+ if (!this.accessor.options.getTabGroupChipContextMenuItems) {
251
+ return;
252
+ }
253
+ const items = this.accessor.options.getTabGroupChipContextMenuItems({
254
+ tabGroup,
255
+ group,
256
+ api: this.accessor.api,
257
+ event,
318
258
  });
319
- }
320
- updateGroupActive() {
321
- this.update({
322
- params: {
323
- isGroupActive: this._group.api.isActive,
324
- },
259
+ if (items.length === 0) {
260
+ return;
261
+ }
262
+ event.preventDefault();
263
+ const popupService = this.accessor.getPopupServiceForGroup(group);
264
+ const close = () => popupService.close();
265
+ const menuEl = document.createElement('div');
266
+ menuEl.className = 'dv-context-menu';
267
+ menuEl.setAttribute('role', 'menu');
268
+ for (const item of items) {
269
+ if (item === 'separator') {
270
+ menuEl.appendChild(buildSeparator());
271
+ }
272
+ else if (item === 'rename') {
273
+ menuEl.appendChild(buildRenameInput(tabGroup));
274
+ }
275
+ else if (item === 'colorPicker') {
276
+ menuEl.appendChild(buildColorPicker(tabGroup, this.accessor.tabGroupColorPalette));
277
+ }
278
+ else if (isItemConfig(item) && item.element) {
279
+ menuEl.appendChild(item.element);
280
+ }
281
+ else if (isItemConfig(item) && item.label) {
282
+ menuEl.appendChild(buildItem(item.label, close, () => { var _a; return (_a = item.action) === null || _a === void 0 ? void 0 : _a.call(item); }, item.disabled));
283
+ }
284
+ }
285
+ popupService.openPopover(menuEl, {
286
+ x: event.clientX,
287
+ y: event.clientY,
288
+ zIndex: popoverZIndexFor(event.target),
325
289
  });
326
290
  }
327
- updateLocation(location) {
328
- this.update({ params: { location } });
329
- }
330
291
  }
292
+ const ContextMenuModule = defineModule({
293
+ name: 'ContextMenu',
294
+ serviceKey: 'contextMenuService',
295
+ create: (host) => new ContextMenuController(host),
296
+ });
331
297
 
332
- class ReactContextMenuItemPart {
333
- get element() {
334
- return this._element;
298
+ /** Cursor offset of the group drag ghost, matched to the long-shipped default. */
299
+ const GROUP_DRAG_GHOST_OFFSET_X = 30;
300
+ const GROUP_DRAG_GHOST_OFFSET_Y = -10;
301
+ /**
302
+ * The narrow surface the {@link AdvancedDnDService} needs from the host
303
+ * (the `DockviewComponent`).
304
+ *
305
+ * The `onWill*` emitters stay on the component so the public event shape is
306
+ * unchanged whether or not this module is registered — the service is only the
307
+ * dispatch point those fires are routed through. Engine policy (e.g. the
308
+ * `disableDnd` guard) stays on the component, ahead of the dispatch.
309
+ */
310
+ /**
311
+ * Owns the dispatch of the advanced drag-and-drop hooks — `onWillDragPanel`,
312
+ * `onWillDragGroup`, `onWillDrop` and `onWillShowOverlay`.
313
+ *
314
+ * At this stage the service is a thin dispatcher that forwards to the host's
315
+ * emitters; the customisation surface it gates (custom drop overlays, custom
316
+ * group drag ghosts, hook veto/transform behaviour) is currently inlined in
317
+ * core and is extracted into this service in later phases. Routing the
318
+ * dispatch through a module slot is what lets that customisation layer move
319
+ * into a separately-distributed package in a future major version without
320
+ * changing the public event surface.
321
+ *
322
+ * The service holds no drag state of its own — the gesture is driven by the
323
+ * DnD backends, and the per-group subscriptions live on the component's group
324
+ * lifecycle (so groups created mid-move are not missed).
325
+ */
326
+ class AdvancedDnDService {
327
+ constructor(host) {
328
+ this.host = host;
335
329
  }
336
- constructor(id, component, reactPortalStore) {
337
- this.id = id;
338
- this.component = component;
339
- this.reactPortalStore = reactPortalStore;
340
- this._element = document.createElement('div');
341
- this._element.className = 'dv-react-part';
342
- this._element.style.height = '100%';
343
- this._element.style.width = '100%';
330
+ dispatchWillDragPanel(event) {
331
+ this.host.fireWillDragPanel(event);
344
332
  }
345
- init(props) {
346
- this.part = new ReactPart(this._element, this.reactPortalStore, this.component, props);
333
+ dispatchWillDragGroup(event) {
334
+ this.host.fireWillDragGroup(event);
347
335
  }
348
- dispose() {
349
- var _a;
350
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.dispose();
336
+ dispatchWillDrop(event) {
337
+ this.host.fireWillDrop(event);
351
338
  }
352
- }
353
-
354
- class ReactTabGroupChipPart {
355
- get element() {
356
- return this._element;
339
+ dispatchWillShowOverlay(event) {
340
+ this.host.fireWillShowOverlay(event);
357
341
  }
358
- constructor(component, reactPortalStore) {
359
- this.component = component;
360
- this.reactPortalStore = reactPortalStore;
361
- this._element = document.createElement('div');
362
- this._element.className = 'dv-react-part';
363
- this._element.style.display = 'inline-flex';
342
+ buildGroupDragGhost(group) {
343
+ const createGhost = this.host.options.createGroupDragGhostComponent;
344
+ if (!createGhost) {
345
+ return undefined;
346
+ }
347
+ const renderer = createGhost(group);
348
+ renderer.init({ group, api: this.host.api });
349
+ return {
350
+ element: renderer.element,
351
+ offsetX: GROUP_DRAG_GHOST_OFFSET_X,
352
+ offsetY: GROUP_DRAG_GHOST_OFFSET_Y,
353
+ dispose: renderer.dispose ? () => { var _a; return (_a = renderer.dispose) === null || _a === void 0 ? void 0 : _a.call(renderer); } : undefined,
354
+ };
364
355
  }
365
- init(params) {
366
- this.part = new ReactPart(this._element, this.reactPortalStore, this.component, {
367
- tabGroup: params.tabGroup,
368
- api: params.api,
369
- });
356
+ resolveOverlayModel(location, group) {
357
+ var _a, _b;
358
+ return (_b = (_a = this.host.options).dropOverlayModel) === null || _b === void 0 ? void 0 : _b.call(_a, { location, group });
370
359
  }
371
- update(params) {
372
- var _a;
373
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.update({ tabGroup: params.tabGroup });
360
+ showPreviewOverlay(group, position) {
361
+ const target = group.model.contentDropTarget;
362
+ target.showOverlay(position);
363
+ return DockviewDisposable.from(() => target.clearOverlay());
374
364
  }
375
365
  dispose() {
376
- var _a;
377
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.dispose();
366
+ // no-op — see class doc: the service holds no state to tear down.
378
367
  }
379
368
  }
369
+ const AdvancedDnDModule = defineModule({
370
+ name: 'AdvancedDnD',
371
+ serviceKey: 'advancedDnDService',
372
+ create: (host) => new AdvancedDnDService(host),
373
+ });
380
374
 
381
- class ReactGroupDragGhostPart {
382
- get element() {
383
- return this._element;
384
- }
385
- constructor(component, reactPortalStore) {
386
- this.component = component;
387
- this.reactPortalStore = reactPortalStore;
388
- this._element = document.createElement('div');
389
- this._element.className = 'dv-react-part';
390
- this._element.style.display = 'inline-flex';
391
- }
392
- init(params) {
393
- this.part = new ReactPart(this._element, this.reactPortalStore, this.component, {
394
- group: params.group,
395
- api: params.api,
396
- });
397
- }
398
- dispose() {
399
- var _a;
400
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.dispose();
375
+ /**
376
+ * Marks (on the dockview root element) that a keyboard move is in progress, so
377
+ * the default navigation listener stands down while the advanced docking module
378
+ * drives the keys. A neutral DOM signal keeps the two listeners coordinated
379
+ * without either service holding a reference to the other.
380
+ */
381
+ const KEYBOARD_MOVE_ATTRIBUTE = 'data-dv-kbd-moving';
382
+ /**
383
+ * Does `e` match a binding string like `'ctrl+]'` / `'shift+f6'`? Modifiers are
384
+ * matched exactly (a binding without `shift` will not fire while Shift is held),
385
+ * and the final part is compared to `KeyboardEvent.key`, lower-cased.
386
+ */
387
+ function matchesBinding(e, binding) {
388
+ const parts = binding.toLowerCase().split('+');
389
+ const key = parts[parts.length - 1];
390
+ const mods = parts.slice(0, -1);
391
+ return (e.ctrlKey === mods.includes('ctrl') &&
392
+ e.shiftKey === mods.includes('shift') &&
393
+ e.altKey === mods.includes('alt') &&
394
+ e.metaKey === (mods.includes('meta') || mods.includes('cmd')) &&
395
+ e.key.toLowerCase() === key);
396
+ }
397
+ /**
398
+ * Resolve the `keyboardNavigation` opt-in to its options object, or `undefined`
399
+ * when keyboard support is off. Both keyboard modules read the same opt-in.
400
+ */
401
+ function readKeyboardNavigation(options) {
402
+ const opt = options.keyboardNavigation;
403
+ if (!opt) {
404
+ return undefined;
401
405
  }
406
+ return opt === true ? {} : opt;
402
407
  }
403
408
 
404
- function createGroupControlElement(component, store) {
405
- return component
406
- ? (groupPanel) => {
407
- return new ReactHeaderActionsRendererPart(component, store, groupPanel);
408
- }
409
- : undefined;
410
- }
411
- const DEFAULT_REACT_TAB = 'props.defaultTabComponent';
412
- function extractCoreOptions$3(props) {
413
- const coreOptions = PROPERTY_KEYS_DOCKVIEW.reduce((obj, key) => {
414
- if (key in props) {
415
- obj[key] = props[key];
416
- }
417
- return obj;
418
- }, {});
419
- return coreOptions;
420
- }
421
- const DockviewReact = React.forwardRef((props, ref) => {
422
- const domRef = React.useRef(null);
423
- const dockviewRef = React.useRef(undefined);
424
- const [portals, addPortal] = usePortalsLifecycle();
425
- React.useImperativeHandle(ref, () => domRef.current, []);
426
- const prevProps = React.useRef({});
427
- React.useEffect(() => {
428
- const changes = {};
429
- PROPERTY_KEYS_DOCKVIEW.forEach((propKey) => {
430
- const key = propKey;
431
- const propValue = props[key];
432
- if (key in props && propValue !== prevProps.current[key]) {
433
- changes[key] = propValue;
409
+ const DEFAULT_KEYMAP$1 = {
410
+ nextTab: 'ctrl+]',
411
+ prevTab: 'ctrl+[',
412
+ focusNextGroup: 'f6',
413
+ focusPrevGroup: 'shift+f6',
414
+ focusTabs: 'ctrl+shift+\\',
415
+ };
416
+ /**
417
+ * Keyboard navigation & focus management — operate the dock without a mouse.
418
+ * Opt-in via `keyboardNavigation`, with a rebindable {@link DockviewKeybindings}
419
+ * keymap.
420
+ *
421
+ * - **Switch tab** (`Ctrl+]` / `Ctrl+[`) — cycle the focused group's tabs.
422
+ * - **Focus group** (`F6` / `Shift+F6`) — move focus to the next / previous
423
+ * group in sequence.
424
+ * - **Focus the tab strip** (`Ctrl+Shift+\`) — jump focus from panel content to
425
+ * the active group's tab strip (the strip's roving-tabindex takes over).
426
+ * - **Focus restore on close** — when removing a panel/group pulls focus out of
427
+ * the dock, focus returns to the neighbour the layout just activated instead
428
+ * of being stranded on `<body>`.
429
+ * - **Floating `Esc`** — `Esc` inside a floating group returns focus to the
430
+ * control that had it before entering the float (polite: bubble phase,
431
+ * respects `defaultPrevented`, so panel content keeps `Esc`).
432
+ * - **Floating Tab-containment** — Tab wraps within the floating group so focus
433
+ * doesn't leak to the grid behind it.
434
+ *
435
+ * Stands down while an advanced keyboard move is in progress (see
436
+ * {@link KEYBOARD_MOVE_ATTRIBUTE}) so the docking module owns the keys then.
437
+ */
438
+ class AccessibilityService extends DockviewCompositeDisposable {
439
+ constructor(host) {
440
+ super();
441
+ this.host = host;
442
+ this._focusWasInside = false;
443
+ // Listen on the document (capture) rather than the dockview element:
444
+ // edge groups live in the shell *outside* the gridview, and the shell
445
+ // is created after this service, so a fixed element would miss them.
446
+ const doc = host.rootElement.ownerDocument;
447
+ const onKeyDown = (e) => this._onKeyDown(e);
448
+ doc.addEventListener('keydown', onKeyDown, true);
449
+ // Remember the last control focused in the main dock (outside any
450
+ // float) so Esc inside a floating group can return focus to its
451
+ // invoking control. Observe-only — never consumes.
452
+ const onFocusIn = (e) => {
453
+ const t = e.target;
454
+ if (t instanceof HTMLElement &&
455
+ this.host.rootElement.contains(t) &&
456
+ !t.closest('[role="dialog"]')) {
457
+ this._lastNonFloatFocus = t;
434
458
  }
435
- });
436
- if (dockviewRef.current) {
437
- dockviewRef.current.updateOptions(changes);
438
- }
439
- prevProps.current = props;
440
- }, PROPERTY_KEYS_DOCKVIEW.map((key) => props[key]));
441
- React.useEffect(() => {
442
- var _a;
443
- if (!domRef.current) {
444
- return;
445
- }
446
- const frameworkTabComponents = (_a = props.tabComponents) !== null && _a !== void 0 ? _a : {};
447
- if (props.defaultTabComponent) {
448
- frameworkTabComponents[DEFAULT_REACT_TAB] =
449
- props.defaultTabComponent;
450
- }
451
- const frameworkOptions = {
452
- createLeftHeaderActionComponent: createGroupControlElement(props.leftHeaderActionsComponent, { addPortal }),
453
- createRightHeaderActionComponent: createGroupControlElement(props.rightHeaderActionsComponent, { addPortal }),
454
- createPrefixHeaderActionComponent: createGroupControlElement(props.prefixHeaderActionsComponent, { addPortal }),
455
- createComponent: (options) => {
456
- return new ReactPanelContentPart(options.id, props.components[options.name], {
457
- addPortal,
458
- });
459
- },
460
- createTabComponent(options) {
461
- return new ReactPanelHeaderPart(options.id, frameworkTabComponents[options.name], {
462
- addPortal,
463
- });
464
- },
465
- createWatermarkComponent: props.watermarkComponent
466
- ? () => {
467
- return new ReactWatermarkPart('watermark', props.watermarkComponent, {
468
- addPortal,
469
- });
470
- }
471
- : undefined,
472
- defaultTabComponent: props.defaultTabComponent
473
- ? DEFAULT_REACT_TAB
474
- : undefined,
475
- createContextMenuItemComponent: (options) => {
476
- if (!options.component) {
477
- return undefined;
478
- }
479
- return new ReactContextMenuItemPart(options.id, options.component, { addPortal });
480
- },
481
- };
482
- const coreOptions = extractCoreOptions$3(props);
483
- if (props.tabGroupChipComponent) {
484
- const chipComponent = props.tabGroupChipComponent;
485
- coreOptions.createTabGroupChipComponent = () => {
486
- return new ReactTabGroupChipPart(chipComponent, {
487
- addPortal,
488
- });
489
- };
490
- }
491
- if (props.groupDragGhostComponent) {
492
- const ghostComponent = props.groupDragGhostComponent;
493
- coreOptions.createGroupDragGhostComponent = () => {
494
- return new ReactGroupDragGhostPart(ghostComponent, {
495
- addPortal,
496
- });
497
- };
498
- }
499
- const api = createDockview(domRef.current, Object.assign(Object.assign({}, coreOptions), frameworkOptions));
500
- const { clientWidth, clientHeight } = domRef.current;
501
- api.layout(clientWidth, clientHeight);
502
- if (props.onReady) {
503
- props.onReady({ api });
504
- }
505
- dockviewRef.current = api;
506
- return () => {
507
- dockviewRef.current = undefined;
508
- api.dispose();
509
459
  };
510
- }, []);
511
- React.useEffect(() => {
512
- if (!dockviewRef.current) {
513
- return () => {
514
- // noop
515
- };
516
- }
517
- const disposable = dockviewRef.current.onDidDrop((event) => {
518
- if (props.onDidDrop) {
519
- props.onDidDrop(event);
460
+ doc.addEventListener('focusin', onFocusIn, true);
461
+ // Esc-from-float restore runs in the BUBBLE phase and respects
462
+ // defaultPrevented, so panel content that uses Esc keeps priority.
463
+ const onEscape = (e) => this._onFloatingEscape(e);
464
+ doc.addEventListener('keydown', onEscape, false);
465
+ this.addDisposables({
466
+ dispose: () => doc.removeEventListener('keydown', onKeyDown, true),
467
+ }, {
468
+ dispose: () => doc.removeEventListener('focusin', onFocusIn, true),
469
+ }, {
470
+ dispose: () => doc.removeEventListener('keydown', onEscape, false),
471
+ },
472
+ // When a close pulls focus out of the dock, return it to the
473
+ // neighbour the component just activated rather than leaving it
474
+ // stranded on <body>. Snapshot before the teardown (focus still on
475
+ // the closing panel), restore after.
476
+ host.onWillMutateLayout((e) => {
477
+ if (e.kind === 'remove' && this._nav) {
478
+ this._focusWasInside = this._isFocusInside();
520
479
  }
521
- });
522
- return () => {
523
- disposable.dispose();
524
- };
525
- }, [props.onDidDrop]);
526
- React.useEffect(() => {
527
- if (!dockviewRef.current) {
528
- return () => {
529
- // noop
530
- };
531
- }
532
- const disposable = dockviewRef.current.onWillDrop((event) => {
533
- if (props.onWillDrop) {
534
- props.onWillDrop(event);
480
+ }), host.onDidMutateLayout((e) => {
481
+ if (e.kind === 'remove' &&
482
+ this._nav &&
483
+ this._focusWasInside &&
484
+ !this._isFocusInside()) {
485
+ this._restoreFocus();
535
486
  }
536
- });
537
- return () => {
538
- disposable.dispose();
539
- };
540
- }, [props.onWillDrop]);
541
- React.useEffect(() => {
542
- if (!dockviewRef.current) {
487
+ }));
488
+ }
489
+ get _moveActive() {
490
+ return this.host.rootElement.hasAttribute(KEYBOARD_MOVE_ATTRIBUTE);
491
+ }
492
+ _isFocusInside() {
493
+ const active = this.host.rootElement.ownerDocument.activeElement;
494
+ return active instanceof Node && this.host.rootElement.contains(active);
495
+ }
496
+ _onFloatingEscape(e) {
497
+ if (!this._nav ||
498
+ this._moveActive ||
499
+ e.defaultPrevented ||
500
+ e.key !== 'Escape') {
543
501
  return;
544
502
  }
545
- dockviewRef.current.updateOptions({
546
- createTabGroupChipComponent: props.tabGroupChipComponent
547
- ? () => {
548
- return new ReactTabGroupChipPart(props.tabGroupChipComponent, {
549
- addPortal,
550
- });
551
- }
552
- : undefined,
553
- });
554
- }, [props.tabGroupChipComponent]);
555
- React.useEffect(() => {
556
- if (!dockviewRef.current) {
503
+ const target = e.target;
504
+ if (!(target instanceof Element)) {
557
505
  return;
558
506
  }
559
- dockviewRef.current.updateOptions({
560
- createGroupDragGhostComponent: props.groupDragGhostComponent
561
- ? () => {
562
- return new ReactGroupDragGhostPart(props.groupDragGhostComponent, {
563
- addPortal,
564
- });
565
- }
566
- : undefined,
567
- });
568
- }, [props.groupDragGhostComponent]);
569
- React.useEffect(() => {
570
- if (!dockviewRef.current) {
507
+ // Only when focus is inside one of *this* dock's floating groups.
508
+ const float = target.closest('[role="dialog"]');
509
+ if (!float || !this.host.rootElement.contains(float)) {
571
510
  return;
572
511
  }
573
- dockviewRef.current.updateOptions({
574
- createComponent: (options) => {
575
- return new ReactPanelContentPart(options.id, props.components[options.name], {
576
- addPortal,
577
- });
578
- },
579
- });
580
- }, [props.components]);
581
- React.useEffect(() => {
512
+ e.preventDefault();
513
+ e.stopPropagation();
514
+ this._returnFocusFromFloat();
515
+ }
516
+ /**
517
+ * Keep Tab inside the floating group that holds focus: at the last tabbable
518
+ * Tab wraps to the first, at the first Shift+Tab wraps to the last. Returns
519
+ * true if it handled the event. No-op outside a float.
520
+ */
521
+ _trapFloatTab(e) {
522
+ const target = e.target;
523
+ if (!(target instanceof Element)) {
524
+ return false;
525
+ }
526
+ const float = target.closest('[role="dialog"]');
527
+ if (!float || !this.host.rootElement.contains(float)) {
528
+ return false;
529
+ }
530
+ // Always manage Tab inside a float, never just at the boundary: focus
531
+ // often sits on non-tabbable plumbing (the content container, which is
532
+ // tabindex="-1"), and the browser's default Tab from there escapes to
533
+ // the grid behind. Drive the cursor through the float's tabbables
534
+ // ourselves and swallow the default so it can't leak out.
535
+ e.preventDefault();
536
+ const tabbables = this._tabbables(float);
537
+ if (tabbables.length === 0) {
538
+ return true;
539
+ }
540
+ const active = float.ownerDocument.activeElement;
541
+ const index = active instanceof HTMLElement ? tabbables.indexOf(active) : -1;
542
+ const n = tabbables.length;
543
+ const next = index === -1
544
+ ? e.shiftKey
545
+ ? n - 1
546
+ : 0
547
+ : (index + (e.shiftKey ? -1 : 1) + n) % n;
548
+ tabbables[next].focus();
549
+ return true;
550
+ }
551
+ _tabbables(root) {
552
+ const nodes = root.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), ' +
553
+ 'select:not([disabled]), textarea:not([disabled]), [tabindex]');
554
+ // tabIndex >= 0 keeps naturally-focusable controls and roving anchors
555
+ // (the active tab) while dropping tabindex="-1" plumbing (content
556
+ // containers, inactive tabs).
557
+ return Array.from(nodes).filter((el) => el.tabIndex >= 0);
558
+ }
559
+ _returnFocusFromFloat() {
582
560
  var _a;
583
- if (!dockviewRef.current) {
561
+ const prev = this._lastNonFloatFocus;
562
+ if (prev &&
563
+ prev.isConnected &&
564
+ this.host.rootElement.contains(prev) &&
565
+ !prev.closest('[role="dialog"]')) {
566
+ prev.focus();
584
567
  return;
585
568
  }
586
- const frameworkTabComponents = (_a = props.tabComponents) !== null && _a !== void 0 ? _a : {};
587
- if (props.defaultTabComponent) {
588
- frameworkTabComponents[DEFAULT_REACT_TAB] =
589
- props.defaultTabComponent;
590
- }
591
- dockviewRef.current.updateOptions({
592
- defaultTabComponent: props.defaultTabComponent
593
- ? DEFAULT_REACT_TAB
594
- : undefined,
595
- createTabComponent(options) {
596
- return new ReactPanelHeaderPart(options.id, frameworkTabComponents[options.name], {
597
- addPortal,
598
- });
599
- },
600
- });
601
- }, [props.tabComponents, props.defaultTabComponent]);
602
- React.useEffect(() => {
603
- if (!dockviewRef.current) {
569
+ // Invoking control is gone fall back to a grid group's content.
570
+ (_a = this.host.groups
571
+ .find((g) => g.api.location.type === 'grid')) === null || _a === void 0 ? void 0 : _a.model.focusContent();
572
+ }
573
+ get _nav() {
574
+ return readKeyboardNavigation(this.host.options);
575
+ }
576
+ get _keymap() {
577
+ var _a;
578
+ return Object.assign(Object.assign({}, DEFAULT_KEYMAP$1), (_a = this._nav) === null || _a === void 0 ? void 0 : _a.keymap);
579
+ }
580
+ _onKeyDown(e) {
581
+ if (!this._nav) {
604
582
  return;
605
583
  }
606
- dockviewRef.current.updateOptions({
607
- createWatermarkComponent: props.watermarkComponent
608
- ? () => {
609
- return new ReactWatermarkPart('watermark', props.watermarkComponent, {
610
- addPortal,
611
- });
612
- }
613
- : undefined,
614
- });
615
- }, [props.watermarkComponent]);
616
- React.useEffect(() => {
617
- if (!dockviewRef.current) {
584
+ // Stand down while the docking module is driving a keyboard move.
585
+ if (this._moveActive) {
618
586
  return;
619
587
  }
620
- dockviewRef.current.updateOptions({
621
- createRightHeaderActionComponent: createGroupControlElement(props.rightHeaderActionsComponent, { addPortal }),
622
- });
623
- }, [props.rightHeaderActionsComponent]);
624
- React.useEffect(() => {
625
- if (!dockviewRef.current) {
588
+ // Only act on events originating inside *this* dockview.
589
+ if (!(e.target instanceof Node) ||
590
+ !this.host.rootElement.contains(e.target)) {
626
591
  return;
627
592
  }
628
- dockviewRef.current.updateOptions({
629
- createLeftHeaderActionComponent: createGroupControlElement(props.leftHeaderActionsComponent, { addPortal }),
630
- });
631
- }, [props.leftHeaderActionsComponent]);
632
- React.useEffect(() => {
633
- if (!dockviewRef.current) {
593
+ // Trap Tab within a floating group so focus doesn't leak to the grid
594
+ // behind it (the float is non-modal, but its Tab order should be).
595
+ if (e.key === 'Tab' && this._trapFloatTab(e)) {
634
596
  return;
635
597
  }
636
- dockviewRef.current.updateOptions({
637
- createPrefixHeaderActionComponent: createGroupControlElement(props.prefixHeaderActionsComponent, { addPortal }),
638
- });
639
- }, [props.prefixHeaderActionsComponent]);
640
- return (React.createElement("div", { style: { height: '100%', width: '100%' }, ref: domRef }, portals));
641
- });
642
- DockviewReact.displayName = 'DockviewComponent';
643
-
644
- /******************************************************************************
645
- Copyright (c) Microsoft Corporation.
646
-
647
- Permission to use, copy, modify, and/or distribute this software for any
648
- purpose with or without fee is hereby granted.
649
-
650
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
651
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
652
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
653
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
654
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
655
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
656
- PERFORMANCE OF THIS SOFTWARE.
657
- ***************************************************************************** */
658
- /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
659
-
660
-
661
- function __rest(s, e) {
662
- var t = {};
663
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
664
- t[p] = s[p];
665
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
666
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
667
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
668
- t[p[i]] = s[p[i]];
669
- }
670
- return t;
671
- }
672
-
673
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
674
- var e = new Error(message);
675
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
676
- };
677
-
678
- const CloseButton = () => (React.createElement("svg", { height: "11", width: "11", viewBox: "0 0 28 28", "aria-hidden": 'false', focusable: false, className: "dv-svg" },
679
- React.createElement("path", { d: "M2.1 27.3L0 25.2L11.55 13.65L0 2.1L2.1 0L13.65 11.55L25.2 0L27.3 2.1L15.75 13.65L27.3 25.2L25.2 27.3L13.65 15.75L2.1 27.3Z" })));
680
-
681
- function useTitle(api) {
682
- const [title, setTitle] = React.useState(api.title);
683
- React.useEffect(() => {
684
- const disposable = api.onDidTitleChange((event) => {
685
- setTitle(event.title);
686
- });
687
- // Depending on the order in which React effects are run, the title may already be out of sync (cf. issue #1003).
688
- if (title !== api.title) {
689
- setTitle(api.title);
598
+ const keymap = this._keymap;
599
+ if (matchesBinding(e, keymap.nextTab)) {
600
+ this._consume(e);
601
+ this._switchTab(false);
690
602
  }
691
- return () => {
692
- disposable.dispose();
693
- };
694
- }, [api]);
695
- return title;
696
- }
697
- const DockviewDefaultTab = (_a) => {
698
- var { api, containerApi: _containerApi, params: _params, hideClose, closeActionOverride, onPointerDown, onPointerUp, onPointerLeave, tabLocation } = _a, rest = __rest(_a, ["api", "containerApi", "params", "hideClose", "closeActionOverride", "onPointerDown", "onPointerUp", "onPointerLeave", "tabLocation"]);
699
- const title = useTitle(api);
700
- const isMiddleMouseButton = React.useRef(false);
701
- const onClose = React.useCallback((event) => {
702
- event.preventDefault();
703
- if (closeActionOverride) {
704
- closeActionOverride();
603
+ else if (matchesBinding(e, keymap.prevTab)) {
604
+ this._consume(e);
605
+ this._switchTab(true);
705
606
  }
706
- else {
707
- api.close();
607
+ else if (matchesBinding(e, keymap.focusNextGroup)) {
608
+ this._consume(e);
609
+ this._cycleGroup(false);
708
610
  }
709
- }, [api, closeActionOverride]);
710
- const onBtnPointerDown = React.useCallback((event) => {
711
- event.preventDefault();
712
- }, []);
713
- const _onPointerDown = React.useCallback((event) => {
714
- isMiddleMouseButton.current = event.button === 1;
715
- onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
716
- }, [onPointerDown]);
717
- const _onPointerUp = React.useCallback((event) => {
718
- if (isMiddleMouseButton && event.button === 1 && !hideClose) {
719
- isMiddleMouseButton.current = false;
720
- onClose(event);
611
+ else if (matchesBinding(e, keymap.focusPrevGroup)) {
612
+ this._consume(e);
613
+ this._cycleGroup(true);
614
+ }
615
+ else if (matchesBinding(e, keymap.focusTabs)) {
616
+ this._consume(e);
617
+ this._focusTabs();
721
618
  }
722
- onPointerUp === null || onPointerUp === void 0 ? void 0 : onPointerUp(event);
723
- }, [onPointerUp, onClose, hideClose]);
724
- const _onPointerLeave = React.useCallback((event) => {
725
- isMiddleMouseButton.current = false;
726
- onPointerLeave === null || onPointerLeave === void 0 ? void 0 : onPointerLeave(event);
727
- }, [onPointerLeave]);
728
- return (React.createElement("div", Object.assign({ "data-testid": "dockview-dv-default-tab" }, rest, { onPointerDown: _onPointerDown, onPointerUp: _onPointerUp, onPointerLeave: _onPointerLeave, className: "dv-default-tab" }),
729
- React.createElement("span", { className: "dv-default-tab-content" }, title),
730
- !hideClose && (React.createElement("div", { className: "dv-default-tab-action", onPointerDown: onBtnPointerDown, onClick: onClose },
731
- React.createElement(CloseButton, null)))));
732
- };
733
-
734
- class ReactPanelView extends SplitviewPanel {
735
- constructor(id, component, reactComponent, reactPortalStore) {
736
- super(id, component);
737
- this.reactComponent = reactComponent;
738
- this.reactPortalStore = reactPortalStore;
739
619
  }
740
- getComponent() {
741
- var _a, _b;
742
- return new ReactPart(this.element, this.reactPortalStore, this.reactComponent, {
743
- params: (_b = (_a = this._params) === null || _a === void 0 ? void 0 : _a.params) !== null && _b !== void 0 ? _b : {},
744
- api: this.api,
745
- containerApi: new SplitviewApi(this._params.accessor),
746
- });
620
+ // --- navigation (uses the public group API + the adjacentGroup primitive) ---
621
+ _focusTabs() {
622
+ var _a;
623
+ // Jump from panel content to the active group's tab strip; the
624
+ // tablist's own roving-tabindex handler takes over from there.
625
+ (_a = this.host.activeGroup) === null || _a === void 0 ? void 0 : _a.model.focusActiveTab();
747
626
  }
748
- }
749
-
750
- function extractCoreOptions$2(props) {
751
- const coreOptions = PROPERTY_KEYS_SPLITVIEW.reduce((obj, key) => {
752
- if (key in props) {
753
- obj[key] = props[key];
754
- }
755
- return obj;
756
- }, {});
757
- return coreOptions;
758
- }
759
- const SplitviewReact = React.forwardRef((props, ref) => {
760
- const domRef = React.useRef(null);
761
- const splitviewRef = React.useRef(undefined);
762
- const [portals, addPortal] = usePortalsLifecycle();
763
- React.useImperativeHandle(ref, () => domRef.current, []);
764
- const prevProps = React.useRef({});
765
- React.useEffect(() => {
766
- const changes = {};
767
- PROPERTY_KEYS_SPLITVIEW.forEach((propKey) => {
768
- const key = propKey;
769
- const propValue = props[key];
770
- if (key in props && propValue !== prevProps.current[key]) {
771
- changes[key] = propValue;
772
- }
773
- });
774
- if (splitviewRef.current) {
775
- splitviewRef.current.updateOptions(changes);
776
- }
777
- prevProps.current = props;
778
- }, PROPERTY_KEYS_SPLITVIEW.map((key) => props[key]));
779
- React.useEffect(() => {
780
- if (!domRef.current) {
781
- return () => {
782
- // noop
783
- };
627
+ _switchTab(reverse) {
628
+ const group = this.host.activeGroup;
629
+ if (!group) {
630
+ return;
784
631
  }
785
- const frameworkOptions = {
786
- createComponent: (options) => {
787
- return new ReactPanelView(options.id, options.name, props.components[options.name], { addPortal });
788
- },
789
- };
790
- const api = createSplitview(domRef.current, Object.assign(Object.assign({}, extractCoreOptions$2(props)), frameworkOptions));
791
- const { clientWidth, clientHeight } = domRef.current;
792
- api.layout(clientWidth, clientHeight);
793
- if (props.onReady) {
794
- props.onReady({ api });
632
+ if (reverse) {
633
+ group.model.moveToPrevious();
795
634
  }
796
- splitviewRef.current = api;
797
- return () => {
798
- splitviewRef.current = undefined;
799
- api.dispose();
800
- };
801
- }, []);
802
- React.useEffect(() => {
803
- if (!splitviewRef.current) {
635
+ else {
636
+ group.model.moveToNext();
637
+ }
638
+ // Keep DOM focus inside the dock: switching hides the previously
639
+ // focused content, which would otherwise drop focus to <body> and
640
+ // leave the keymap unable to see the next key.
641
+ group.model.focusContent();
642
+ }
643
+ _cycleGroup(reverse) {
644
+ const current = this.host.activeGroup;
645
+ const target = current
646
+ ? this.host.adjacentGroup(current, reverse)
647
+ : this.host.groups[0];
648
+ this._focusGroup(target);
649
+ }
650
+ _focusGroup(target) {
651
+ if (!target) {
804
652
  return;
805
653
  }
806
- splitviewRef.current.updateOptions({
807
- createComponent: (options) => {
808
- return new ReactPanelView(options.id, options.name, props.components[options.name], { addPortal });
809
- },
810
- });
811
- }, [props.components]);
812
- return (React.createElement("div", { style: { height: '100%', width: '100%' }, ref: domRef }, portals));
813
- });
814
- SplitviewReact.displayName = 'SplitviewComponent';
815
-
816
- class ReactGridPanelView extends GridviewPanel {
817
- constructor(id, component, reactComponent, reactPortalStore) {
818
- super(id, component);
819
- this.reactComponent = reactComponent;
820
- this.reactPortalStore = reactPortalStore;
654
+ target.api.setActive();
655
+ target.model.focusContent();
821
656
  }
822
- getComponent() {
823
- var _a, _b;
824
- return new ReactPart(this.element, this.reactPortalStore, this.reactComponent, {
825
- params: (_b = (_a = this._params) === null || _a === void 0 ? void 0 : _a.params) !== null && _b !== void 0 ? _b : {},
826
- api: this.api,
827
- // TODO: fix casting hack
828
- containerApi: new GridviewApi(this._params
829
- .accessor),
830
- });
657
+ /** Return DOM focus to the active group's content, keeping it in the dock. */
658
+ _restoreFocus() {
659
+ var _a;
660
+ (_a = this.host.activeGroup) === null || _a === void 0 ? void 0 : _a.model.focusContent();
661
+ }
662
+ _consume(e) {
663
+ e.preventDefault();
664
+ e.stopPropagation();
831
665
  }
832
666
  }
667
+ const AccessibilityModule = defineModule({
668
+ name: 'Accessibility',
669
+ serviceKey: 'accessibilityService',
670
+ create: (host) => new AccessibilityService(host),
671
+ });
833
672
 
834
- function extractCoreOptions$1(props) {
835
- const coreOptions = PROPERTY_KEYS_GRIDVIEW.reduce((obj, key) => {
836
- if (key in props) {
837
- obj[key] = props[key];
673
+ const EDGE_FROM_KEY = {
674
+ ArrowLeft: 'left',
675
+ ArrowRight: 'right',
676
+ ArrowUp: 'top',
677
+ ArrowDown: 'bottom',
678
+ };
679
+ const DEFAULT_KEYMAP = {
680
+ focusGroupLeft: 'ctrl+shift+arrowleft',
681
+ focusGroupRight: 'ctrl+shift+arrowright',
682
+ focusGroupUp: 'ctrl+shift+arrowup',
683
+ focusGroupDown: 'ctrl+shift+arrowdown',
684
+ dock: 'ctrl+m',
685
+ };
686
+ /**
687
+ * Advanced keyboard control — drive the layout itself without a mouse. Opt-in
688
+ * via `keyboardNavigation` (shared with the default navigation module).
689
+ *
690
+ * - **Spatial group focus** (`Ctrl+Shift+Arrows`) — move focus to the visually
691
+ * adjacent group via the `adjacentGroupInDirection` geometry primitive.
692
+ * - **Keyboard docking** (`Ctrl+M`) — arms a two-phase move of the active panel
693
+ * with a live drop preview + screen-reader narration:
694
+ * 1. PICK TARGET — arrows cycle the groups (incl. the panel's own, so a tab
695
+ * can be split out); `Enter` selects one.
696
+ * 2. PICK EDGE — arrows choose a split edge (left/right/top/bottom) or the
697
+ * centre (tab-into); `Enter` commits, `Escape` steps back.
698
+ * `Escape` from the target phase cancels.
699
+ *
700
+ * While a move is in progress it marks the root with
701
+ * {@link KEYBOARD_MOVE_ATTRIBUTE} so the default navigation listener stands
702
+ * down and the move keys aren't double-handled.
703
+ */
704
+ class KeyboardDockingService extends DockviewCompositeDisposable {
705
+ constructor(host) {
706
+ super();
707
+ this.host = host;
708
+ this._move = null;
709
+ // Capture phase, on the document — matches the navigation listener so
710
+ // edge groups (outside the gridview) are covered.
711
+ const doc = host.rootElement.ownerDocument;
712
+ const onKeyDown = (e) => this._onKeyDown(e);
713
+ doc.addEventListener('keydown', onKeyDown, true);
714
+ this.addDisposables({ dispose: () => this._exit() }, {
715
+ dispose: () => doc.removeEventListener('keydown', onKeyDown, true),
716
+ });
717
+ }
718
+ get _enabled() {
719
+ return !!readKeyboardNavigation(this.host.options);
720
+ }
721
+ get _keymap() {
722
+ var _a, _b, _c, _d, _e, _f, _g;
723
+ // The public `keyboardNavigation.keymap` carries the default-navigation
724
+ // bindings; this module's bindings are read from the same object when
725
+ // present (so a consumer can rebind them too).
726
+ const overrides = ((_b = (_a = readKeyboardNavigation(this.host.options)) === null || _a === void 0 ? void 0 : _a.keymap) !== null && _b !== void 0 ? _b : {});
727
+ return {
728
+ focusGroupLeft: (_c = overrides.focusGroupLeft) !== null && _c !== void 0 ? _c : DEFAULT_KEYMAP.focusGroupLeft,
729
+ focusGroupRight: (_d = overrides.focusGroupRight) !== null && _d !== void 0 ? _d : DEFAULT_KEYMAP.focusGroupRight,
730
+ focusGroupUp: (_e = overrides.focusGroupUp) !== null && _e !== void 0 ? _e : DEFAULT_KEYMAP.focusGroupUp,
731
+ focusGroupDown: (_f = overrides.focusGroupDown) !== null && _f !== void 0 ? _f : DEFAULT_KEYMAP.focusGroupDown,
732
+ dock: (_g = overrides.dock) !== null && _g !== void 0 ? _g : DEFAULT_KEYMAP.dock,
733
+ };
734
+ }
735
+ get _messages() {
736
+ return resolveMessages(this.host.options.messages);
737
+ }
738
+ _onKeyDown(e) {
739
+ if (!this._enabled) {
740
+ return;
838
741
  }
839
- return obj;
840
- }, {});
841
- return coreOptions;
842
- }
843
- const GridviewReact = React.forwardRef((props, ref) => {
844
- const domRef = React.useRef(null);
845
- const gridviewRef = React.useRef(undefined);
846
- const [portals, addPortal] = usePortalsLifecycle();
847
- React.useImperativeHandle(ref, () => domRef.current, []);
848
- const prevProps = React.useRef({});
849
- React.useEffect(() => {
850
- const changes = {};
851
- PROPERTY_KEYS_GRIDVIEW.forEach((propKey) => {
852
- const key = propKey;
853
- const propValue = props[key];
854
- if (key in props && propValue !== prevProps.current[key]) {
855
- changes[key] = propValue;
856
- }
857
- });
858
- if (gridviewRef.current) {
859
- gridviewRef.current.updateOptions(changes);
742
+ if (this._move) {
743
+ this._onMoveKey(e);
744
+ return;
860
745
  }
861
- prevProps.current = props;
862
- }, PROPERTY_KEYS_GRIDVIEW.map((key) => props[key]));
863
- React.useEffect(() => {
864
- if (!domRef.current) {
865
- return () => {
866
- // noop
867
- };
746
+ // Only act on events originating inside *this* dockview.
747
+ if (!(e.target instanceof Node) ||
748
+ !this.host.rootElement.contains(e.target)) {
749
+ return;
868
750
  }
869
- const frameworkOptions = {
870
- createComponent: (options) => {
871
- return new ReactGridPanelView(options.id, options.name, props.components[options.name], { addPortal });
872
- },
873
- };
874
- const api = createGridview(domRef.current, Object.assign(Object.assign({}, extractCoreOptions$1(props)), frameworkOptions));
875
- const { clientWidth, clientHeight } = domRef.current;
876
- api.layout(clientWidth, clientHeight);
877
- if (props.onReady) {
878
- props.onReady({ api });
751
+ const keymap = this._keymap;
752
+ if (matchesBinding(e, keymap.dock)) {
753
+ this._enterMoveMode(e);
879
754
  }
880
- gridviewRef.current = api;
881
- return () => {
882
- gridviewRef.current = undefined;
883
- api.dispose();
884
- };
885
- }, []);
886
- React.useEffect(() => {
887
- if (!gridviewRef.current) {
888
- return;
755
+ else if (matchesBinding(e, keymap.focusGroupLeft)) {
756
+ this._consume(e);
757
+ this._focusGroupInDirection('left');
758
+ }
759
+ else if (matchesBinding(e, keymap.focusGroupRight)) {
760
+ this._consume(e);
761
+ this._focusGroupInDirection('right');
762
+ }
763
+ else if (matchesBinding(e, keymap.focusGroupUp)) {
764
+ this._consume(e);
765
+ this._focusGroupInDirection('up');
766
+ }
767
+ else if (matchesBinding(e, keymap.focusGroupDown)) {
768
+ this._consume(e);
769
+ this._focusGroupInDirection('down');
889
770
  }
890
- gridviewRef.current.updateOptions({
891
- createComponent: (options) => {
892
- return new ReactGridPanelView(options.id, options.name, props.components[options.name], { addPortal });
893
- },
894
- });
895
- }, [props.components]);
896
- return (React.createElement("div", { style: { height: '100%', width: '100%' }, ref: domRef }, portals));
897
- });
898
- GridviewReact.displayName = 'GridviewComponent';
899
-
900
- class PanePanelSection {
901
- get element() {
902
- return this._element;
903
- }
904
- constructor(id, component, reactPortalStore) {
905
- this.id = id;
906
- this.component = component;
907
- this.reactPortalStore = reactPortalStore;
908
- this._element = document.createElement('div');
909
- this._element.style.height = '100%';
910
- this._element.style.width = '100%';
911
- }
912
- init(parameters) {
913
- this.part = new ReactPart(this.element, this.reactPortalStore, this.component, {
914
- params: parameters.params,
915
- api: parameters.api,
916
- title: parameters.title,
917
- containerApi: parameters.containerApi,
918
- });
919
771
  }
920
- toJSON() {
921
- return {
922
- id: this.id,
923
- };
772
+ // --- spatial focus ---
773
+ _focusGroupInDirection(direction) {
774
+ const current = this.host.activeGroup;
775
+ if (!current) {
776
+ return;
777
+ }
778
+ // Geometry lives on the host as the shared `adjacentGroupInDirection`
779
+ // primitive (also public on the api), so mouse and keyboard navigation
780
+ // agree on what "the group to the left" is.
781
+ this._focusGroup(this.host.adjacentGroupInDirection(current, direction));
924
782
  }
925
- update(params) {
926
- var _a;
927
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.update(params.params);
783
+ _focusGroup(target) {
784
+ if (!target) {
785
+ return;
786
+ }
787
+ target.api.setActive();
788
+ target.model.focusContent();
928
789
  }
929
- dispose() {
790
+ _restoreFocus() {
930
791
  var _a;
931
- (_a = this.part) === null || _a === void 0 ? void 0 : _a.dispose();
792
+ (_a = this.host.activeGroup) === null || _a === void 0 ? void 0 : _a.model.focusContent();
932
793
  }
933
- }
934
-
935
- function extractCoreOptions(props) {
936
- const coreOptions = PROPERTY_KEYS_PANEVIEW.reduce((obj, key) => {
937
- if (key in props) {
938
- obj[key] = props[key];
939
- }
940
- return obj;
941
- }, {});
942
- return coreOptions;
943
- }
944
- const PaneviewReact = React.forwardRef((props, ref) => {
945
- const domRef = React.useRef(null);
946
- const paneviewRef = React.useRef(undefined);
947
- const [portals, addPortal] = usePortalsLifecycle();
948
- React.useImperativeHandle(ref, () => domRef.current, []);
949
- const prevProps = React.useRef({});
950
- React.useEffect(() => {
951
- const changes = {};
952
- PROPERTY_KEYS_PANEVIEW.forEach((propKey) => {
953
- const key = propKey;
954
- const propValue = props[key];
955
- if (key in props && propValue !== prevProps.current[key]) {
956
- changes[key] = propValue;
957
- }
958
- });
959
- if (paneviewRef.current) {
960
- paneviewRef.current.updateOptions(changes);
961
- }
962
- prevProps.current = props;
963
- }, PROPERTY_KEYS_PANEVIEW.map((key) => props[key]));
964
- React.useEffect(() => {
965
- var _a;
966
- if (!domRef.current) {
967
- return () => {
968
- // noop
969
- };
794
+ // --- keyboard docking (move mode) ---
795
+ _enterMoveMode(e) {
796
+ const source = this.host.activePanel;
797
+ const groups = this.host.groups;
798
+ if (!source || groups.length === 0) {
799
+ return;
970
800
  }
971
- const headerComponents = (_a = props.headerComponents) !== null && _a !== void 0 ? _a : {};
972
- const frameworkOptions = {
973
- createComponent: (options) => {
974
- return new PanePanelSection(options.id, props.components[options.name], { addPortal });
975
- },
976
- createHeaderComponent: (options) => {
977
- return new PanePanelSection(options.id, headerComponents[options.name], { addPortal });
978
- },
801
+ e.preventDefault();
802
+ e.stopPropagation();
803
+ // Start on the panel's own group so a single group of tabs can still
804
+ // split a tab out via the edge phase.
805
+ const groupIndex = Math.max(0, groups.indexOf(source.group));
806
+ this._move = {
807
+ source,
808
+ groups,
809
+ groupIndex,
810
+ phase: 'target',
811
+ position: 'center',
979
812
  };
980
- const api = createPaneview(domRef.current, Object.assign(Object.assign({}, extractCoreOptions(props)), frameworkOptions));
981
- const { clientWidth, clientHeight } = domRef.current;
982
- api.layout(clientWidth, clientHeight);
983
- if (props.onReady) {
984
- props.onReady({ api });
813
+ // Signal the navigation module to stand down while the move runs.
814
+ this.host.rootElement.setAttribute(KEYBOARD_MOVE_ATTRIBUTE, '');
815
+ this._render();
816
+ }
817
+ _onMoveKey(e) {
818
+ const move = this._move;
819
+ if (e.key === 'Escape') {
820
+ this._consume(e);
821
+ if (move.phase === 'edge') {
822
+ move.phase = 'target';
823
+ move.position = 'center';
824
+ this._render();
825
+ }
826
+ else {
827
+ this._exit();
828
+ this.host.announce(this._messages.moveCancelled());
829
+ this._restoreFocus();
830
+ }
831
+ return;
985
832
  }
986
- paneviewRef.current = api;
987
- return () => {
988
- paneviewRef.current = undefined;
989
- api.dispose();
990
- };
991
- }, []);
992
- React.useEffect(() => {
993
- if (!paneviewRef.current) {
833
+ if (e.key === 'Enter') {
834
+ this._consume(e);
835
+ if (move.phase === 'target') {
836
+ move.phase = 'edge';
837
+ move.position = 'center';
838
+ this._render();
839
+ }
840
+ else {
841
+ this._commit();
842
+ }
994
843
  return;
995
844
  }
996
- paneviewRef.current.updateOptions({
997
- createComponent: (options) => {
998
- return new PanePanelSection(options.id, props.components[options.name], { addPortal });
999
- },
1000
- });
1001
- }, [props.components]);
1002
- React.useEffect(() => {
1003
- var _a;
1004
- if (!paneviewRef.current) {
845
+ if (move.phase === 'target') {
846
+ const n = move.groups.length;
847
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
848
+ this._consume(e);
849
+ move.groupIndex = (move.groupIndex + 1) % n;
850
+ this._render();
851
+ }
852
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
853
+ this._consume(e);
854
+ move.groupIndex = (move.groupIndex - 1 + n) % n;
855
+ this._render();
856
+ }
1005
857
  return;
1006
858
  }
1007
- const headerComponents = (_a = props.headerComponents) !== null && _a !== void 0 ? _a : {};
1008
- paneviewRef.current.updateOptions({
1009
- createHeaderComponent: (options) => {
1010
- return new PanePanelSection(options.id, headerComponents[options.name], { addPortal });
1011
- },
1012
- });
1013
- }, [props.headerComponents]);
1014
- React.useEffect(() => {
1015
- if (!paneviewRef.current) {
1016
- return () => {
1017
- // noop
1018
- };
859
+ // edge phase
860
+ const edge = EDGE_FROM_KEY[e.key];
861
+ if (edge) {
862
+ this._consume(e);
863
+ move.position = edge;
864
+ this._render();
1019
865
  }
1020
- const disposable = paneviewRef.current.onDidDrop((event) => {
1021
- if (props.onDidDrop) {
1022
- props.onDidDrop(event);
1023
- }
1024
- });
1025
- return () => {
1026
- disposable.dispose();
1027
- };
1028
- }, [props.onDidDrop]);
1029
- return (React.createElement("div", { style: { height: '100%', width: '100%' }, ref: domRef }, portals));
866
+ else if (e.key === ' ' || e.key === 'c' || e.key === 'C') {
867
+ this._consume(e);
868
+ move.position = 'center';
869
+ this._render();
870
+ }
871
+ }
872
+ _render() {
873
+ var _a, _b;
874
+ const move = this._move;
875
+ const group = move.groups[move.groupIndex];
876
+ this._clearPreview();
877
+ this._preview = this.host.showDropPreview(group, move.position);
878
+ const name = (_b = (_a = group.activePanel) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : group.id;
879
+ const m = this._messages;
880
+ if (move.phase === 'target') {
881
+ this.host.announce(m.movePickTarget(this._label(move.source), name, move.groupIndex + 1, move.groups.length));
882
+ }
883
+ else {
884
+ this.host.announce(m.movePickEdge(move.position, name));
885
+ }
886
+ }
887
+ _commit() {
888
+ var _a, _b;
889
+ const move = this._move;
890
+ const group = move.groups[move.groupIndex];
891
+ const position = move.position;
892
+ const source = move.source;
893
+ const name = (_b = (_a = group.activePanel) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : group.id;
894
+ const m = this._messages;
895
+ this._exit();
896
+ try {
897
+ this.host.dockPanel(source, group, position);
898
+ this.host.announce(m.moveCommitted(this._label(source), name, position));
899
+ }
900
+ catch (_c) {
901
+ this.host.announce(m.moveNotAllowed());
902
+ }
903
+ // The move re-renders the grid; pull focus back into the dock so the
904
+ // keymap keeps working without a click.
905
+ this._restoreFocus();
906
+ }
907
+ _exit() {
908
+ this._clearPreview();
909
+ this._move = null;
910
+ this.host.rootElement.removeAttribute(KEYBOARD_MOVE_ATTRIBUTE);
911
+ }
912
+ _clearPreview() {
913
+ var _a;
914
+ (_a = this._preview) === null || _a === void 0 ? void 0 : _a.dispose();
915
+ this._preview = undefined;
916
+ }
917
+ _consume(e) {
918
+ e.preventDefault();
919
+ e.stopPropagation();
920
+ }
921
+ _label(panel) {
922
+ var _a;
923
+ return (_a = panel.title) !== null && _a !== void 0 ? _a : panel.id;
924
+ }
925
+ }
926
+ defineModule({
927
+ name: 'KeyboardDocking',
928
+ serviceKey: 'keyboardDockingService',
929
+ create: (host) => new KeyboardDockingService(host),
930
+ dependsOn: [AdvancedDnDModule, LiveRegionModule],
1030
931
  });
1031
- PaneviewReact.displayName = 'PaneviewComponent';
1032
932
 
1033
- export { DockviewDefaultTab, DockviewReact, GridviewReact, PaneviewReact, ReactPart, ReactPartContext, SplitviewReact, isReactComponent, usePortalsLifecycle };
933
+ /**
934
+ * The set of modules provided by this package. `dockview` registers these via
935
+ * `registerModules(Modules)` so they are available to every component.
936
+ */
937
+ const Modules = [
938
+ TabGroupChipsModule,
939
+ ContextMenuModule,
940
+ AdvancedDnDModule,
941
+ AccessibilityModule,
942
+ ];
943
+
944
+ /**
945
+ * `dockview` is the batteries-included entry point: it re-exports the core API
946
+ * and registers the separable feature modules so every component gets the full
947
+ * feature set out of the box. Consumers who want only the core can depend on
948
+ * `dockview-core` directly.
949
+ */
950
+ registerModules(Modules);
951
+ // Mark the public package as loaded so `dockview-core` doesn't warn about
952
+ // direct usage. Purely drives that developer warning — no functional effect.
953
+ markDockviewPackageLoaded();