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