@octanejs/resizable-panels 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE.upstream +21 -0
  2. package/README.md +22 -0
  3. package/UPSTREAM.md +64 -0
  4. package/package.json +61 -0
  5. package/src/components/group/Group.tsrx +244 -0
  6. package/src/components/group/GroupContext.ts +25 -0
  7. package/src/components/group/auto-save/getStorageKey.ts +3 -0
  8. package/src/components/group/auto-save/readLegacyLayout.ts +37 -0
  9. package/src/components/group/sortByElementOffset.ts +20 -0
  10. package/src/components/group/types.ts +50 -0
  11. package/src/components/group/useDefaultLayout.ts +152 -0
  12. package/src/components/group/useGroupCallbackRef.ts +6 -0
  13. package/src/components/group/useGroupContext.ts +12 -0
  14. package/src/components/group/useGroupImperativeHandle.ts +46 -0
  15. package/src/components/group/useGroupRef.ts +6 -0
  16. package/src/components/panel/Panel.tsrx +130 -0
  17. package/src/components/panel/types.ts +66 -0
  18. package/src/components/panel/usePanelCallbackRef.ts +6 -0
  19. package/src/components/panel/usePanelImperativeHandle.ts +42 -0
  20. package/src/components/panel/usePanelRef.ts +6 -0
  21. package/src/components/separator/Separator.tsrx +126 -0
  22. package/src/components/separator/types.ts +22 -0
  23. package/src/constants.ts +25 -0
  24. package/src/global/cursor/getCursorStyle.ts +102 -0
  25. package/src/global/cursor/supportsAdvancedCursorStyles.ts +25 -0
  26. package/src/global/cursor/updateCursorStyle.ts +77 -0
  27. package/src/global/dom/calculateAvailableGroupSize.ts +11 -0
  28. package/src/global/dom/calculateHitRegions.ts +200 -0
  29. package/src/global/dom/calculatePanelConstraints.ts +84 -0
  30. package/src/global/event-handlers/onDocumentContextMenu.ts +9 -0
  31. package/src/global/event-handlers/onDocumentDoubleClick.ts +31 -0
  32. package/src/global/event-handlers/onDocumentKeyDown.ts +133 -0
  33. package/src/global/event-handlers/onDocumentPointerDown.ts +56 -0
  34. package/src/global/event-handlers/onDocumentPointerLeave.ts +21 -0
  35. package/src/global/event-handlers/onDocumentPointerMove.ts +89 -0
  36. package/src/global/event-handlers/onDocumentPointerOut.ts +19 -0
  37. package/src/global/event-handlers/onDocumentPointerUp.ts +14 -0
  38. package/src/global/mountGroup.ts +213 -0
  39. package/src/global/mutable-state/groups.ts +104 -0
  40. package/src/global/mutable-state/interactions.ts +49 -0
  41. package/src/global/mutable-state/types.ts +31 -0
  42. package/src/global/styles/convertEmToPixels.ts +6 -0
  43. package/src/global/styles/convertRemToPixels.ts +6 -0
  44. package/src/global/styles/convertVhToPixels.ts +3 -0
  45. package/src/global/styles/convertVwToPixels.ts +3 -0
  46. package/src/global/styles/parseSizeAndUnit.ts +28 -0
  47. package/src/global/styles/sizeStyleToPixels.ts +48 -0
  48. package/src/global/utils/adjustLayoutByDelta.ts +339 -0
  49. package/src/global/utils/adjustLayoutForSeparator.ts +54 -0
  50. package/src/global/utils/calculateDefaultLayout.ts +37 -0
  51. package/src/global/utils/calculateSeparatorAriaValues.ts +67 -0
  52. package/src/global/utils/compareLayoutNumbers.ts +9 -0
  53. package/src/global/utils/completeActivePointerResize.ts +37 -0
  54. package/src/global/utils/doRectsIntersect.ts +5 -0
  55. package/src/global/utils/findClosestHitRegion.ts +43 -0
  56. package/src/global/utils/findClosestRect.ts +36 -0
  57. package/src/global/utils/findMatchingHitRegions.ts +41 -0
  58. package/src/global/utils/findSeparatorGroup.ts +13 -0
  59. package/src/global/utils/formatLayoutNumber.ts +3 -0
  60. package/src/global/utils/getDistanceBetweenPointAndRect.ts +14 -0
  61. package/src/global/utils/getImperativeGroupMethods.ts +68 -0
  62. package/src/global/utils/getImperativePanelMethods.ts +228 -0
  63. package/src/global/utils/isCoarsePointer.ts +8 -0
  64. package/src/global/utils/isViableHitTarget.ts +53 -0
  65. package/src/global/utils/layoutNumbersEqual.ts +5 -0
  66. package/src/global/utils/layoutsEqual.ts +17 -0
  67. package/src/global/utils/notifyPanelOnResize.ts +33 -0
  68. package/src/global/utils/objectsEqual.ts +14 -0
  69. package/src/global/utils/preserveFixedPanelSizes.ts +74 -0
  70. package/src/global/utils/updateActiveHitRegion.ts +132 -0
  71. package/src/global/utils/validateLayoutKeys.ts +19 -0
  72. package/src/global/utils/validatePanelGroupLayout.ts +92 -0
  73. package/src/global/utils/validatePanelSize.ts +41 -0
  74. package/src/hooks/useForceUpdate.ts +8 -0
  75. package/src/hooks/useId.ts +6 -0
  76. package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
  77. package/src/hooks/useMergedRefs.ts +18 -0
  78. package/src/hooks/useStableArray.ts +15 -0
  79. package/src/hooks/useStableCallback.ts +18 -0
  80. package/src/hooks/useStableObject.ts +15 -0
  81. package/src/index.tsrx +12 -0
  82. package/src/internal.ts +23 -0
  83. package/src/types.ts +18 -0
  84. package/src/utils/EventEmitter.ts +72 -0
  85. package/src/utils/assert.ts +8 -0
  86. package/src/utils/isArrayEqual.ts +12 -0
  87. package/src/utils/isHTMLElement.ts +9 -0
  88. package/src/utils/isShadowRoot.ts +9 -0
  89. package/src/utils/parseNumericStyleValue.ts +15 -0
  90. package/src/vendor/stacking-order.ts +133 -0
  91. package/status.json +11 -0
@@ -0,0 +1,102 @@
1
+ import type { RegisteredGroup } from '../../components/group/types';
2
+ import {
3
+ CURSOR_FLAG_HORIZONTAL_MAX,
4
+ CURSOR_FLAG_HORIZONTAL_MIN,
5
+ CURSOR_FLAG_VERTICAL_MAX,
6
+ CURSOR_FLAG_VERTICAL_MIN,
7
+ } from '../../constants';
8
+ import type { InteractionState } from '../mutable-state/types';
9
+ import { supportsAdvancedCursorStyles } from './supportsAdvancedCursorStyles';
10
+
11
+ export function getCursorStyle({
12
+ cursorFlags,
13
+ groups,
14
+ state,
15
+ }: {
16
+ cursorFlags: number;
17
+ groups: RegisteredGroup[];
18
+ state: InteractionState['state'];
19
+ }): CSSStyleDeclaration['cursor'] | undefined {
20
+ let horizontalCount = 0;
21
+ let verticalCount = 0;
22
+
23
+ switch (state) {
24
+ case 'active':
25
+ case 'hover': {
26
+ groups.forEach((group) => {
27
+ if (group.mutableState.disableCursor) {
28
+ return;
29
+ }
30
+
31
+ switch (group.orientation) {
32
+ case 'horizontal': {
33
+ horizontalCount++;
34
+ break;
35
+ }
36
+ case 'vertical': {
37
+ verticalCount++;
38
+ break;
39
+ }
40
+ }
41
+ });
42
+ }
43
+ }
44
+
45
+ if (horizontalCount === 0 && verticalCount === 0) {
46
+ return undefined;
47
+ }
48
+
49
+ switch (state) {
50
+ case 'active': {
51
+ if (cursorFlags) {
52
+ if (supportsAdvancedCursorStyles()) {
53
+ const horizontalMin = (cursorFlags & CURSOR_FLAG_HORIZONTAL_MIN) !== 0;
54
+ const horizontalMax = (cursorFlags & CURSOR_FLAG_HORIZONTAL_MAX) !== 0;
55
+ const verticalMin = (cursorFlags & CURSOR_FLAG_VERTICAL_MIN) !== 0;
56
+ const verticalMax = (cursorFlags & CURSOR_FLAG_VERTICAL_MAX) !== 0;
57
+
58
+ if (horizontalMin) {
59
+ if (verticalMin) {
60
+ return 'se-resize';
61
+ } else if (verticalMax) {
62
+ return 'ne-resize';
63
+ } else {
64
+ return 'e-resize';
65
+ }
66
+ } else if (horizontalMax) {
67
+ if (verticalMin) {
68
+ return 'sw-resize';
69
+ } else if (verticalMax) {
70
+ return 'nw-resize';
71
+ } else {
72
+ return 'w-resize';
73
+ }
74
+ } else if (verticalMin) {
75
+ return 's-resize';
76
+ } else if (verticalMax) {
77
+ return 'n-resize';
78
+ }
79
+ }
80
+ }
81
+ break;
82
+ }
83
+ }
84
+
85
+ if (supportsAdvancedCursorStyles()) {
86
+ if (horizontalCount > 0 && verticalCount > 0) {
87
+ return 'move';
88
+ } else if (horizontalCount > 0) {
89
+ return 'ew-resize';
90
+ } else {
91
+ return 'ns-resize';
92
+ }
93
+ } else {
94
+ if (horizontalCount > 0 && verticalCount > 0) {
95
+ return 'grab';
96
+ } else if (horizontalCount > 0) {
97
+ return 'col-resize';
98
+ } else {
99
+ return 'row-resize';
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,25 @@
1
+ let cached: boolean | undefined = undefined;
2
+
3
+ export function overrideSupportsAdvancedCursorStylesForTesting(override: boolean) {
4
+ cached = override;
5
+ }
6
+
7
+ /**
8
+ * Caches and returns if advanced cursor CSS styles are supported.
9
+ */
10
+ export function supportsAdvancedCursorStyles(): boolean {
11
+ if (cached === undefined) {
12
+ cached = false;
13
+
14
+ if (typeof window !== 'undefined') {
15
+ if (
16
+ window.navigator.userAgent.includes('Chrome') ||
17
+ window.navigator.userAgent.includes('Firefox')
18
+ ) {
19
+ cached = true;
20
+ }
21
+ }
22
+ }
23
+
24
+ return cached;
25
+ }
@@ -0,0 +1,77 @@
1
+ import { getInteractionState } from '../mutable-state/interactions';
2
+ import { getCursorStyle } from './getCursorStyle';
3
+
4
+ const documentToStyleMap = new WeakMap<
5
+ Document,
6
+ {
7
+ prevStyle: string | undefined;
8
+ styleSheet: CSSStyleSheet;
9
+ }
10
+ >();
11
+
12
+ export function updateCursorStyle(ownerDocument: Document) {
13
+ // NOTE undefined is not technically a valid value but it has been reported that it is present in some environments (Vite HMR?)
14
+ // See github.com/bvaughn/react-resizable-panels/issues/559
15
+ if (ownerDocument.defaultView === null || ownerDocument.defaultView === undefined) {
16
+ return;
17
+ }
18
+
19
+ let { prevStyle, styleSheet } = documentToStyleMap.get(ownerDocument) ?? {};
20
+
21
+ if (styleSheet === undefined) {
22
+ styleSheet = new ownerDocument.defaultView.CSSStyleSheet();
23
+
24
+ // adoptedStyleSheets is undefined in jsdom
25
+ if (ownerDocument.adoptedStyleSheets) {
26
+ if (Object.isExtensible(ownerDocument.adoptedStyleSheets)) {
27
+ ownerDocument.adoptedStyleSheets.push(styleSheet);
28
+ } else {
29
+ ownerDocument.adoptedStyleSheets = [...ownerDocument.adoptedStyleSheets, styleSheet];
30
+ }
31
+ }
32
+ }
33
+
34
+ const interactionState = getInteractionState();
35
+
36
+ switch (interactionState.state) {
37
+ case 'active':
38
+ case 'hover': {
39
+ const cursorStyle = getCursorStyle({
40
+ cursorFlags: interactionState.cursorFlags,
41
+ groups: interactionState.hitRegions.map((current) => current.group),
42
+ state: interactionState.state,
43
+ });
44
+
45
+ const nextStyle = `*, *:hover {cursor: ${cursorStyle} !important; }`;
46
+ if (prevStyle === nextStyle) {
47
+ return;
48
+ }
49
+
50
+ prevStyle = nextStyle;
51
+
52
+ if (cursorStyle) {
53
+ if (styleSheet.cssRules.length === 0) {
54
+ styleSheet.insertRule(nextStyle);
55
+ } else {
56
+ styleSheet.replaceSync(nextStyle);
57
+ }
58
+ } else if (styleSheet.cssRules.length === 1) {
59
+ styleSheet.deleteRule(0);
60
+ }
61
+ break;
62
+ }
63
+ case 'inactive': {
64
+ prevStyle = undefined;
65
+
66
+ if (styleSheet.cssRules.length === 1) {
67
+ styleSheet.deleteRule(0);
68
+ }
69
+ break;
70
+ }
71
+ }
72
+
73
+ documentToStyleMap.set(ownerDocument, {
74
+ prevStyle,
75
+ styleSheet,
76
+ });
77
+ }
@@ -0,0 +1,11 @@
1
+ import type { RegisteredGroup } from '../../components/group/types';
2
+
3
+ export function calculateAvailableGroupSize({ group }: { group: RegisteredGroup }) {
4
+ const { orientation, panels } = group;
5
+
6
+ return panels.reduce((totalSize, panel) => {
7
+ totalSize +=
8
+ orientation === 'horizontal' ? panel.element.offsetWidth : panel.element.offsetHeight;
9
+ return totalSize;
10
+ }, 0);
11
+ }
@@ -0,0 +1,200 @@
1
+ import { sortByElementOffset } from '../../components/group/sortByElementOffset';
2
+ import type { RegisteredGroup } from '../../components/group/types';
3
+ import type { RegisteredPanel } from '../../components/panel/types';
4
+ import type { RegisteredSeparator } from '../../components/separator/types';
5
+ import { isHTMLElement } from '../../utils/isHTMLElement';
6
+ import { findClosestRect } from '../utils/findClosestRect';
7
+ import { isCoarsePointer } from '../utils/isCoarsePointer';
8
+ import { calculateAvailableGroupSize } from './calculateAvailableGroupSize';
9
+
10
+ type PanelsTuple = [panel: RegisteredPanel, panel: RegisteredPanel];
11
+
12
+ export type HitRegion = {
13
+ group: RegisteredGroup;
14
+ groupSize: number;
15
+ panels: PanelsTuple;
16
+ rect: DOMRect;
17
+ separator?: RegisteredSeparator | undefined;
18
+ };
19
+
20
+ /**
21
+ * Determines hit regions for a Group; a hit region is either:
22
+ * - 1: An explicit Separator element
23
+ * - 2: The edge of a Panel element that has another Panel beside it
24
+ *
25
+ * This method determines bounding rects of all regions for the particular group.
26
+ */
27
+ export function calculateHitRegions(group: RegisteredGroup) {
28
+ const { element: groupElement, orientation, panels, separators } = group;
29
+
30
+ // Sort elements by offset before traversing
31
+ const sortedChildElements: HTMLElement[] = sortByElementOffset(
32
+ orientation,
33
+ Array.from(groupElement.children)
34
+ .filter(isHTMLElement)
35
+ .map((element) => ({ element: element as HTMLElement })),
36
+ ).map(({ element }) => element);
37
+
38
+ const hitRegions: HitRegion[] = [];
39
+
40
+ let disabledSeparator = false;
41
+ let hasInterleavedStaticContent = false;
42
+ let firstEnabledPanelIndex = -1;
43
+ let lastEnabledPanelIndex = -1;
44
+ let numEnabledPanels = 0;
45
+ let prevPanel: RegisteredPanel | undefined = undefined;
46
+ let pendingSeparators: RegisteredSeparator[] = [];
47
+
48
+ {
49
+ let currentPanelIndex = -1;
50
+
51
+ for (const childElement of sortedChildElements) {
52
+ if (childElement.hasAttribute('data-panel')) {
53
+ currentPanelIndex++;
54
+
55
+ if (!childElement.hasAttribute('data-disabled')) {
56
+ numEnabledPanels++;
57
+
58
+ if (firstEnabledPanelIndex === -1) {
59
+ firstEnabledPanelIndex = currentPanelIndex;
60
+ }
61
+
62
+ lastEnabledPanelIndex = currentPanelIndex;
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ // If all (or all but one) of the Panels are disabled, there can be no resize interactions.
69
+ if (numEnabledPanels > 1) {
70
+ let currentPanelIndex = -1;
71
+
72
+ for (const childElement of sortedChildElements) {
73
+ if (childElement.hasAttribute('data-panel')) {
74
+ currentPanelIndex++;
75
+
76
+ const panelData = panels.find((current) => current.element === childElement);
77
+ if (panelData) {
78
+ if (prevPanel) {
79
+ const prevRect = prevPanel.element.getBoundingClientRect();
80
+ const rect = childElement.getBoundingClientRect();
81
+
82
+ let pendingRectsOrSeparators: (DOMRect | RegisteredSeparator)[];
83
+
84
+ // If an explicit Separator has been rendered, always watch it
85
+ // Otherwise watch the entire space between the panels
86
+ // The one caveat is when there are non-interactive element(s) between panels,
87
+ // in which case we may need to watch individual panel edges
88
+ if (hasInterleavedStaticContent) {
89
+ const firstPanelEdgeRect =
90
+ orientation === 'horizontal'
91
+ ? new DOMRect(prevRect.right, prevRect.top, 0, prevRect.height)
92
+ : new DOMRect(prevRect.left, prevRect.bottom, prevRect.width, 0);
93
+ const secondPanelEdgeRect =
94
+ orientation === 'horizontal'
95
+ ? new DOMRect(rect.left, rect.top, 0, rect.height)
96
+ : new DOMRect(rect.left, rect.top, rect.width, 0);
97
+
98
+ switch (pendingSeparators.length) {
99
+ case 0: {
100
+ pendingRectsOrSeparators = [firstPanelEdgeRect, secondPanelEdgeRect];
101
+ break;
102
+ }
103
+ case 1: {
104
+ const separator = pendingSeparators[0];
105
+ const closestRect = findClosestRect({
106
+ orientation,
107
+ rects: [prevRect, rect],
108
+ targetRect: separator.element.getBoundingClientRect(),
109
+ });
110
+
111
+ pendingRectsOrSeparators = [
112
+ separator,
113
+ closestRect === prevRect ? secondPanelEdgeRect : firstPanelEdgeRect,
114
+ ];
115
+ break;
116
+ }
117
+ default: {
118
+ pendingRectsOrSeparators = pendingSeparators;
119
+ break;
120
+ }
121
+ }
122
+ } else {
123
+ if (pendingSeparators.length) {
124
+ pendingRectsOrSeparators = pendingSeparators;
125
+ } else {
126
+ pendingRectsOrSeparators = [
127
+ orientation === 'horizontal'
128
+ ? new DOMRect(prevRect.right, rect.top, rect.left - prevRect.right, rect.height)
129
+ : new DOMRect(
130
+ rect.left,
131
+ prevRect.bottom,
132
+ rect.width,
133
+ rect.top - prevRect.bottom,
134
+ ),
135
+ ];
136
+ }
137
+ }
138
+
139
+ for (const rectOrSeparator of pendingRectsOrSeparators) {
140
+ let rect =
141
+ 'width' in rectOrSeparator
142
+ ? rectOrSeparator
143
+ : rectOrSeparator.element.getBoundingClientRect();
144
+
145
+ const minHitTargetSize = isCoarsePointer()
146
+ ? group.resizeTargetMinimumSize.coarse
147
+ : group.resizeTargetMinimumSize.fine;
148
+ if (rect.width < minHitTargetSize) {
149
+ const delta = minHitTargetSize - rect.width;
150
+ rect = new DOMRect(rect.x - delta / 2, rect.y, rect.width + delta, rect.height);
151
+ }
152
+ if (rect.height < minHitTargetSize) {
153
+ const delta = minHitTargetSize - rect.height;
154
+ rect = new DOMRect(rect.x, rect.y - delta / 2, rect.width, rect.height + delta);
155
+ }
156
+
157
+ const skip =
158
+ currentPanelIndex <= firstEnabledPanelIndex ||
159
+ currentPanelIndex > lastEnabledPanelIndex;
160
+
161
+ if (!disabledSeparator && !skip) {
162
+ hitRegions.push({
163
+ group,
164
+ groupSize: calculateAvailableGroupSize({ group }),
165
+ panels: [prevPanel, panelData],
166
+ separator: 'width' in rectOrSeparator ? undefined : rectOrSeparator,
167
+ rect,
168
+ });
169
+ }
170
+
171
+ disabledSeparator = false;
172
+ }
173
+ }
174
+
175
+ hasInterleavedStaticContent = false;
176
+ prevPanel = panelData;
177
+ pendingSeparators = [];
178
+ }
179
+ } else if (childElement.hasAttribute('data-separator')) {
180
+ if (childElement.ariaDisabled !== null) {
181
+ disabledSeparator = true;
182
+ }
183
+
184
+ const separatorData = separators.find((current) => current.element === childElement);
185
+ if (separatorData) {
186
+ // Separators will be included implicitly in the area between the previous and next panel
187
+ // It's important to track them though, to handle the scenario of non-interactive group content
188
+ pendingSeparators.push(separatorData);
189
+ } else {
190
+ prevPanel = undefined;
191
+ pendingSeparators = [];
192
+ }
193
+ } else {
194
+ hasInterleavedStaticContent = true;
195
+ }
196
+ }
197
+ }
198
+
199
+ return hitRegions;
200
+ }
@@ -0,0 +1,84 @@
1
+ import type { RegisteredGroup } from '../../components/group/types';
2
+ import type { PanelConstraints } from '../../components/panel/types';
3
+ import { sizeStyleToPixels } from '../styles/sizeStyleToPixels';
4
+ import { formatLayoutNumber } from '../utils/formatLayoutNumber';
5
+ import { calculateAvailableGroupSize } from './calculateAvailableGroupSize';
6
+
7
+ export function calculatePanelConstraints(group: RegisteredGroup) {
8
+ const { panels } = group;
9
+
10
+ const groupSize = calculateAvailableGroupSize({ group });
11
+ if (groupSize === 0) {
12
+ // Can't calculate anything meaningful if the group has a width/height of 0
13
+ // (This could indicate that it's within a hidden subtree)
14
+ return panels.map<PanelConstraints>((current) => ({
15
+ groupResizeBehavior: current.panelConstraints.groupResizeBehavior,
16
+ collapsedSize: 0,
17
+ collapsible: current.panelConstraints.collapsible === true,
18
+ defaultSize: undefined,
19
+ disabled: current.panelConstraints.disabled,
20
+ minSize: 0,
21
+ maxSize: 100,
22
+ panelId: current.id,
23
+ }));
24
+ }
25
+
26
+ return panels.map<PanelConstraints>((panel) => {
27
+ const { element, panelConstraints } = panel;
28
+
29
+ let collapsedSize = 0;
30
+ if (panelConstraints.collapsedSize !== undefined) {
31
+ const pixels = sizeStyleToPixels({
32
+ groupSize,
33
+ panelElement: element,
34
+ styleProp: panelConstraints.collapsedSize,
35
+ });
36
+
37
+ collapsedSize = formatLayoutNumber((pixels / groupSize) * 100);
38
+ }
39
+
40
+ let defaultSize: number | undefined = undefined;
41
+ if (panelConstraints.defaultSize !== undefined) {
42
+ const pixels = sizeStyleToPixels({
43
+ groupSize,
44
+ panelElement: element,
45
+ styleProp: panelConstraints.defaultSize,
46
+ });
47
+
48
+ defaultSize = formatLayoutNumber((pixels / groupSize) * 100);
49
+ }
50
+
51
+ let minSize = 0;
52
+ if (panelConstraints.minSize !== undefined) {
53
+ const pixels = sizeStyleToPixels({
54
+ groupSize,
55
+ panelElement: element,
56
+ styleProp: panelConstraints.minSize,
57
+ });
58
+
59
+ minSize = formatLayoutNumber((pixels / groupSize) * 100);
60
+ }
61
+
62
+ let maxSize = 100;
63
+ if (panelConstraints.maxSize !== undefined) {
64
+ const pixels = sizeStyleToPixels({
65
+ groupSize,
66
+ panelElement: element,
67
+ styleProp: panelConstraints.maxSize,
68
+ });
69
+
70
+ maxSize = formatLayoutNumber((pixels / groupSize) * 100);
71
+ }
72
+
73
+ return {
74
+ groupResizeBehavior: panelConstraints.groupResizeBehavior,
75
+ collapsedSize,
76
+ collapsible: panelConstraints.collapsible === true,
77
+ defaultSize,
78
+ disabled: panelConstraints.disabled,
79
+ minSize,
80
+ maxSize,
81
+ panelId: panel.id,
82
+ };
83
+ });
84
+ }
@@ -0,0 +1,9 @@
1
+ import { completeActivePointerResize } from '../utils/completeActivePointerResize.ts';
2
+
3
+ export function onDocumentContextMenu(event: MouseEvent) {
4
+ if (event.defaultPrevented) {
5
+ return;
6
+ }
7
+
8
+ completeActivePointerResize(event.currentTarget as Document);
9
+ }
@@ -0,0 +1,31 @@
1
+ import { getMountedGroups } from '../mutable-state/groups';
2
+ import { findMatchingHitRegions } from '../utils/findMatchingHitRegions';
3
+ import { getImperativePanelMethods } from '../utils/getImperativePanelMethods';
4
+
5
+ export function onDocumentDoubleClick(event: MouseEvent) {
6
+ if (event.defaultPrevented) {
7
+ return;
8
+ }
9
+
10
+ const mountedGroups = getMountedGroups();
11
+ const hitRegions = findMatchingHitRegions(event, mountedGroups);
12
+ hitRegions.forEach((current) => {
13
+ if (current.separator && !current.separator.disableDoubleClick) {
14
+ const panelWithDefaultSize = current.panels.find(
15
+ (panel) => panel.panelConstraints.defaultSize !== undefined,
16
+ );
17
+ if (panelWithDefaultSize) {
18
+ const defaultSize = panelWithDefaultSize.panelConstraints.defaultSize;
19
+ const api = getImperativePanelMethods({
20
+ groupId: current.group.id,
21
+ panelId: panelWithDefaultSize.id,
22
+ });
23
+ if (api && defaultSize !== undefined) {
24
+ api.resize(defaultSize);
25
+
26
+ event.preventDefault();
27
+ }
28
+ }
29
+ }
30
+ });
31
+ }
@@ -0,0 +1,133 @@
1
+ import { assert } from '../../utils/assert';
2
+ import { getMountedGroupState } from '../mutable-state/groups';
3
+ import { adjustLayoutForSeparator } from '../utils/adjustLayoutForSeparator';
4
+ import { findSeparatorGroup } from '../utils/findSeparatorGroup';
5
+
6
+ export function onDocumentKeyDown(event: KeyboardEvent) {
7
+ if (event.defaultPrevented) {
8
+ return;
9
+ }
10
+
11
+ const separatorElement = event.currentTarget as HTMLElement;
12
+
13
+ const group = findSeparatorGroup(separatorElement);
14
+ if (group.disabled) {
15
+ return;
16
+ }
17
+
18
+ switch (event.key) {
19
+ case 'ArrowDown': {
20
+ event.preventDefault();
21
+
22
+ if (group.orientation === 'vertical') {
23
+ adjustLayoutForSeparator(separatorElement, 5);
24
+ }
25
+ break;
26
+ }
27
+ case 'ArrowLeft': {
28
+ event.preventDefault();
29
+
30
+ if (group.orientation === 'horizontal') {
31
+ adjustLayoutForSeparator(separatorElement, -5);
32
+ }
33
+ break;
34
+ }
35
+ case 'ArrowRight': {
36
+ event.preventDefault();
37
+
38
+ if (group.orientation === 'horizontal') {
39
+ adjustLayoutForSeparator(separatorElement, 5);
40
+ }
41
+ break;
42
+ }
43
+ case 'ArrowUp': {
44
+ event.preventDefault();
45
+
46
+ if (group.orientation === 'vertical') {
47
+ adjustLayoutForSeparator(separatorElement, -5);
48
+ }
49
+ break;
50
+ }
51
+ case 'End': {
52
+ event.preventDefault();
53
+
54
+ // Moves splitter to the position that gives the primary pane its largest allowed size.
55
+ // This may completely collapse the secondary pane.
56
+
57
+ adjustLayoutForSeparator(separatorElement, 100);
58
+ break;
59
+ }
60
+ case 'Enter': {
61
+ event.preventDefault();
62
+
63
+ // If the primary pane is not collapsed, collapses the pane.
64
+ // If the pane is collapsed, restores the splitter to its previous position.
65
+
66
+ const group = findSeparatorGroup(separatorElement);
67
+
68
+ const groupState = getMountedGroupState(group.id, true);
69
+ const { derivedPanelConstraints, layout, separatorToPanels } = groupState;
70
+
71
+ const separator = group.separators.find((current) => current.element === separatorElement);
72
+ assert(separator, 'Matching separator not found');
73
+
74
+ const panels = separatorToPanels.get(separator);
75
+ assert(panels, 'Matching panels not found');
76
+
77
+ const primaryPanel = panels[0];
78
+ const constraints = derivedPanelConstraints.find(
79
+ (current) => current.panelId === primaryPanel.id,
80
+ );
81
+ assert(constraints, 'Panel metadata not found');
82
+
83
+ if (constraints.collapsible) {
84
+ const prevSize = layout[primaryPanel.id];
85
+
86
+ const nextSize =
87
+ constraints.collapsedSize === prevSize
88
+ ? (group.mutableState.expandedPanelSizes[primaryPanel.id] ?? constraints.minSize)
89
+ : constraints.collapsedSize;
90
+
91
+ adjustLayoutForSeparator(separatorElement, nextSize - prevSize);
92
+ }
93
+ break;
94
+ }
95
+ case 'F6': {
96
+ event.preventDefault();
97
+
98
+ // Cycle through window panes.
99
+
100
+ const group = findSeparatorGroup(separatorElement);
101
+
102
+ const separatorElements = group.separators.map((separator) => separator.element);
103
+
104
+ const index = Array.from(separatorElements).findIndex(
105
+ (current) => current === event.currentTarget,
106
+ );
107
+ assert(index !== null, 'Index not found');
108
+
109
+ const nextIndex = event.shiftKey
110
+ ? index > 0
111
+ ? index - 1
112
+ : separatorElements.length - 1
113
+ : index + 1 < separatorElements.length
114
+ ? index + 1
115
+ : 0;
116
+
117
+ const nextSeparatorElement = separatorElements[nextIndex] as HTMLElement;
118
+ nextSeparatorElement.focus({
119
+ preventScroll: true,
120
+ });
121
+ break;
122
+ }
123
+ case 'Home': {
124
+ event.preventDefault();
125
+
126
+ // Moves splitter to the position that gives the primary pane its smallest allowed size.
127
+ // This may completely collapse the primary pane.
128
+
129
+ adjustLayoutForSeparator(separatorElement, -100);
130
+ break;
131
+ }
132
+ }
133
+ }