@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,152 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'octane';
2
+ import { splitSlot, subSlot } from '../../internal';
3
+ import { getStorageKey } from './auto-save/getStorageKey';
4
+ import { readLegacyLayout } from './auto-save/readLegacyLayout';
5
+ import type { Layout, LayoutChangedMeta, LayoutStorage } from './types';
6
+
7
+ type UseDefaultLayoutOptions = {
8
+ debounceSaveMs?: number;
9
+ onlySaveAfterUserInteractions?: boolean;
10
+ panelIds?: string[];
11
+ storage?: LayoutStorage;
12
+ } & ({ groupId: string } | { id: string });
13
+
14
+ export function useDefaultLayout(options: UseDefaultLayoutOptions, ...rest: [symbol?]) {
15
+ const [args, slot] = splitSlot(rest);
16
+ if (args.length !== 0) throw new TypeError('useDefaultLayout() accepts one options argument.');
17
+ const {
18
+ debounceSaveMs = 100,
19
+ onlySaveAfterUserInteractions,
20
+ panelIds,
21
+ storage: storageProp,
22
+ ...identity
23
+ } = options;
24
+ const id = 'id' in identity ? identity.id : identity.groupId;
25
+ const hasPanelIds = panelIds !== undefined;
26
+ const storage = resolveStorage(storageProp);
27
+ const readStorageKey = getStorageKey(id, panelIds ?? []);
28
+ // Match upstream: the same storage read runs on server and client so
29
+ // cookie/DB-backed LayoutStorage can restore during SSR. Client-only APIs
30
+ // (implicit localStorage) resolve to undefined on the server via
31
+ // resolveStorage, so the snapshot stays null without a custom storage prop.
32
+ const defaultLayoutString = useSyncExternalStore(
33
+ subscribe,
34
+ () => readDefaultLayoutString(storage, readStorageKey, id, panelIds),
35
+ () => readDefaultLayoutString(storage, readStorageKey, id, panelIds),
36
+ subSlot(slot, 'stored-layout'),
37
+ );
38
+ const defaultLayout = useMemo(
39
+ () => readModernLayout(defaultLayoutString),
40
+ [defaultLayoutString],
41
+ subSlot(slot, 'default-layout'),
42
+ );
43
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null, subSlot(slot, 'timeout'));
44
+ const storageRef = useRef<LayoutStorage | undefined>(storage, subSlot(slot, 'storage'));
45
+ storageRef.current = storage;
46
+
47
+ const clearPendingTimeout = useCallback(
48
+ () => {
49
+ if (timeoutRef.current !== null) {
50
+ clearTimeout(timeoutRef.current);
51
+ timeoutRef.current = null;
52
+ }
53
+ },
54
+ [],
55
+ subSlot(slot, 'clear-timeout'),
56
+ );
57
+
58
+ useEffect(() => clearPendingTimeout, [clearPendingTimeout], subSlot(slot, 'cleanup-effect'));
59
+
60
+ const onLayoutChanged = useCallback<(layout: Layout, meta: LayoutChangedMeta) => void>(
61
+ (layout: Layout, meta: LayoutChangedMeta) => {
62
+ if (onlySaveAfterUserInteractions && !meta.isUserInteraction) return;
63
+ clearPendingTimeout();
64
+
65
+ const storage = storageRef.current;
66
+ if (!storage) return;
67
+ const key = getStorageKey(id, hasPanelIds ? Object.keys(layout) : []);
68
+ try {
69
+ storage.setItem(key, JSON.stringify(layout));
70
+ } catch (error) {
71
+ console.error(error);
72
+ }
73
+ },
74
+ [clearPendingTimeout, hasPanelIds, id, onlySaveAfterUserInteractions],
75
+ subSlot(slot, 'layout-changed'),
76
+ );
77
+
78
+ const onLayoutChange = useCallback<(layout: Layout) => void>(
79
+ (layout: Layout) => {
80
+ clearPendingTimeout();
81
+ if (debounceSaveMs === 0) {
82
+ onLayoutChanged(layout, { isUserInteraction: false });
83
+ } else {
84
+ timeoutRef.current = setTimeout(
85
+ () => onLayoutChanged(layout, { isUserInteraction: false }),
86
+ debounceSaveMs,
87
+ );
88
+ }
89
+ },
90
+ [clearPendingTimeout, debounceSaveMs, onLayoutChanged],
91
+ subSlot(slot, 'layout-change'),
92
+ );
93
+
94
+ return { defaultLayout, onLayoutChange, onLayoutChanged };
95
+ }
96
+
97
+ function subscribe() {
98
+ return () => {};
99
+ }
100
+
101
+ function resolveStorage(storage: LayoutStorage | undefined): LayoutStorage | undefined {
102
+ if (storage) return storage;
103
+ try {
104
+ return globalThis.localStorage;
105
+ } catch {
106
+ return undefined;
107
+ }
108
+ }
109
+
110
+ function readStorageItem(storage: LayoutStorage | undefined, key: string): string | null {
111
+ if (!storage) return null;
112
+ try {
113
+ return storage.getItem(key);
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ function readDefaultLayoutString(
120
+ storage: LayoutStorage | undefined,
121
+ modernKey: string,
122
+ id: string,
123
+ panelIds: readonly string[] | undefined,
124
+ ): string | null {
125
+ const modernString = readStorageItem(storage, modernKey);
126
+ if (readModernLayout(modernString)) return modernString;
127
+ if (!storage) return null;
128
+ try {
129
+ const legacy = readLegacyLayout({ id, panelIds, storage });
130
+ return legacy ? JSON.stringify(legacy) : null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ function readModernLayout(value: string | null): Layout | undefined {
137
+ if (!value) return undefined;
138
+ try {
139
+ const parsed: unknown = JSON.parse(value);
140
+ if (
141
+ parsed !== null &&
142
+ typeof parsed === 'object' &&
143
+ !Array.isArray(parsed) &&
144
+ Object.values(parsed).every((item) => typeof item === 'number')
145
+ ) {
146
+ return parsed as Layout;
147
+ }
148
+ } catch {
149
+ // Malformed modern records fall through to the legacy reader.
150
+ }
151
+ return undefined;
152
+ }
@@ -0,0 +1,6 @@
1
+ import { useState } from 'octane';
2
+ import type { GroupImperativeHandle } from './types';
3
+
4
+ export function useGroupCallbackRef(slot?: symbol) {
5
+ return useState<GroupImperativeHandle | null>(null, slot);
6
+ }
@@ -0,0 +1,12 @@
1
+ import { useContext } from 'octane';
2
+ import { GroupContext } from './GroupContext';
3
+
4
+ export function useGroupContext(_slot?: symbol) {
5
+ const value = useContext(GroupContext);
6
+ if (value === null) {
7
+ throw new Error(
8
+ 'Group Context not found; did you render a Panel or Separator outside of a Group?',
9
+ );
10
+ }
11
+ return value;
12
+ }
@@ -0,0 +1,46 @@
1
+ import { useRef } from 'octane';
2
+ import { IDENTITY_FUNCTION } from '../../constants';
3
+ import { getImperativeGroupMethods } from '../../global/utils/getImperativeGroupMethods';
4
+ import { subSlot } from '../../internal';
5
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
6
+ import type { GroupImperativeHandle, GroupProps } from './types';
7
+
8
+ export function useGroupImperativeHandle(
9
+ groupId: string,
10
+ groupRef: GroupProps['groupRef'],
11
+ slot?: symbol,
12
+ ) {
13
+ const imperativeGroupRef = useRef<GroupImperativeHandle>(
14
+ {
15
+ getLayout: () => ({}),
16
+ setLayout: IDENTITY_FUNCTION,
17
+ },
18
+ subSlot(slot, 'ref'),
19
+ );
20
+
21
+ useIsomorphicLayoutEffect(
22
+ () => {
23
+ Object.assign(imperativeGroupRef.current, getImperativeGroupMethods({ groupId }));
24
+ assignRef(groupRef, imperativeGroupRef.current);
25
+ return () => assignRef(groupRef, null);
26
+ },
27
+ [groupId, groupRef],
28
+ subSlot(slot, 'effect'),
29
+ );
30
+ }
31
+
32
+ function assignRef(ref: GroupProps['groupRef'], value: GroupImperativeHandle | null): void {
33
+ if (isRefArray(ref)) {
34
+ for (const nested of ref) assignRef(nested, value);
35
+ } else if (typeof ref === 'function') {
36
+ (ref as (next: GroupImperativeHandle | null) => void)(value);
37
+ } else if (ref) {
38
+ (ref as { current: GroupImperativeHandle | null }).current = value;
39
+ }
40
+ }
41
+
42
+ function isRefArray(
43
+ ref: GroupProps['groupRef'],
44
+ ): ref is readonly NonNullable<GroupProps['groupRef']>[] {
45
+ return Array.isArray(ref);
46
+ }
@@ -0,0 +1,6 @@
1
+ import { useRef } from 'octane';
2
+ import type { GroupImperativeHandle } from './types';
3
+
4
+ export function useGroupRef(slot?: symbol) {
5
+ return useRef<GroupImperativeHandle | null>(null, slot);
6
+ }
@@ -0,0 +1,130 @@
1
+ import { useEffect, useRef, useSyncExternalStore } from 'octane';
2
+ import { subscribeToMountedGroup } from '../../global/mutable-state/groups';
3
+ import { useId } from '../../hooks/useId';
4
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
5
+ import { useMergedRefs } from '../../hooks/useMergedRefs';
6
+ import { useStableCallback } from '../../hooks/useStableCallback';
7
+ import { useStableObject } from '../../hooks/useStableObject';
8
+ import { useGroupContext } from '../group/useGroupContext';
9
+ import type { PanelProps, PanelSize, RegisteredPanel } from './types';
10
+ import { usePanelImperativeHandle } from './usePanelImperativeHandle';
11
+
12
+ export function Panel(props: PanelProps) @{
13
+ const {
14
+ children,
15
+ className,
16
+ collapsedSize = '0%',
17
+ collapsible = false,
18
+ defaultSize,
19
+ disabled,
20
+ elementRef: elementRefProp,
21
+ groupResizeBehavior = 'preserve-relative-size',
22
+ id: idProp,
23
+ maxSize = '100%',
24
+ minSize = '0%',
25
+ onResize: onResizeUnstable,
26
+ panelRef,
27
+ style,
28
+ ...rest
29
+ } = props;
30
+ const idIsStable = !!idProp;
31
+ const id = useId(idProp);
32
+ const stableProps = useStableObject({ disabled });
33
+ const elementRef = useRef<HTMLDivElement | null>(null);
34
+ const mergedRef = useMergedRefs(elementRef, elementRefProp);
35
+ const { getPanelStyles, id: groupId, orientation, registerPanel, updatePanelProps } =
36
+ useGroupContext();
37
+ const hasOnResize = onResizeUnstable != null;
38
+ const onResizeStable = useStableCallback((
39
+ panelSize: PanelSize,
40
+ _id: string | number | undefined,
41
+ prevPanelSize: PanelSize | undefined,
42
+ ) => {
43
+ onResizeUnstable?.(panelSize, idProp, prevPanelSize);
44
+ });
45
+
46
+ useIsomorphicLayoutEffect(() => {
47
+ const element = elementRef.current;
48
+ if (element === null) return;
49
+ const registeredPanel: RegisteredPanel = {
50
+ element,
51
+ id,
52
+ idIsStable,
53
+ mutableValues: { expandToSize: undefined, prevSize: undefined },
54
+ onResize: hasOnResize ? onResizeStable : undefined,
55
+ panelConstraints: {
56
+ groupResizeBehavior,
57
+ collapsedSize,
58
+ collapsible,
59
+ defaultSize,
60
+ disabled: stableProps.disabled,
61
+ maxSize,
62
+ minSize,
63
+ },
64
+ };
65
+ return registerPanel(registeredPanel);
66
+ }, [groupResizeBehavior, collapsedSize, collapsible, defaultSize, hasOnResize, id, idIsStable, maxSize, minSize, onResizeStable, registerPanel, stableProps]);
67
+
68
+ useEffect(() => updatePanelProps(id, { disabled }), [disabled, id, updatePanelProps]);
69
+ usePanelImperativeHandle(id, panelRef);
70
+
71
+ const read = () => {
72
+ const panelStyles = getPanelStyles(groupId, id);
73
+ return panelStyles === undefined ? undefined : JSON.stringify(panelStyles);
74
+ };
75
+ const panelStylesString = useSyncExternalStore(
76
+ (subscribe) => subscribeToMountedGroup(groupId, () => subscribe()),
77
+ read,
78
+ read,
79
+ );
80
+ const panelStyles =
81
+ panelStylesString
82
+ ? JSON.parse(panelStylesString) as Record<string, unknown>
83
+ : defaultSize !== undefined
84
+ ? { flexGrow: undefined, flexShrink: undefined, flexBasis: defaultSize }
85
+ : { flexGrow: 1 };
86
+
87
+ return <div
88
+ {...rest}
89
+ data-disabled={disabled || undefined}
90
+ data-panel
91
+ data-testid={id}
92
+ id={id}
93
+ ref={mergedRef}
94
+ style={{
95
+ ...PROHIBITED_CSS_PROPERTIES,
96
+ display: 'flex',
97
+ flexBasis: 0,
98
+ flexShrink: 1,
99
+ overflow: 'visible',
100
+ ...panelStyles,
101
+ }}
102
+ >
103
+ <div
104
+ className={className}
105
+ style={{
106
+ maxHeight: '100%',
107
+ maxWidth: '100%',
108
+ flexGrow: 1,
109
+ overflow: 'auto',
110
+ ...style,
111
+ touchAction: orientation === 'horizontal' ? 'pan-y' : 'pan-x',
112
+ }}
113
+ >{children}</div>
114
+ </div>;
115
+ }
116
+
117
+ (Panel as { displayName?: string }).displayName = 'Panel';
118
+
119
+ const PROHIBITED_CSS_PROPERTIES = {
120
+ minHeight: 0,
121
+ maxHeight: '100%',
122
+ height: 'auto',
123
+ minWidth: 0,
124
+ maxWidth: '100%',
125
+ width: 'auto',
126
+ border: 'none',
127
+ borderWidth: 0,
128
+ padding: 0,
129
+ margin: 0,
130
+ } as const;
@@ -0,0 +1,66 @@
1
+ import type { OctaneNode } from 'octane';
2
+ import type { Octane } from 'octane/jsx-runtime';
3
+
4
+ export type PanelSize = { asPercentage: number; inPixels: number };
5
+ export type GroupResizeBehavior = 'preserve-relative-size' | 'preserve-pixel-size';
6
+ export type SizeUnit = 'px' | '%' | 'em' | 'rem' | 'vh' | 'vw';
7
+ export type PanelConstraintProps = {
8
+ collapsedSize?: number | string;
9
+ collapsible?: boolean;
10
+ defaultSize?: number | string;
11
+ disabled?: boolean;
12
+ groupResizeBehavior?: GroupResizeBehavior;
13
+ maxSize?: number | string;
14
+ minSize?: number | string;
15
+ };
16
+ export type PanelConstraints = {
17
+ collapsedSize: number;
18
+ collapsible: boolean;
19
+ defaultSize: number | undefined;
20
+ disabled: boolean | undefined;
21
+ groupResizeBehavior?: GroupResizeBehavior;
22
+ maxSize: number;
23
+ minSize: number;
24
+ panelId: string;
25
+ };
26
+ type PanelResizeCallback = (
27
+ size: PanelSize,
28
+ id: string | number | undefined,
29
+ previousSize: PanelSize | undefined,
30
+ ) => void;
31
+ export type RegisteredPanel = {
32
+ id: string;
33
+ idIsStable: boolean;
34
+ element: HTMLDivElement;
35
+ mutableValues: { expandToSize: number | undefined; prevSize: PanelSize | undefined };
36
+ onResize: PanelResizeCallback | undefined;
37
+ panelConstraints: PanelConstraintProps;
38
+ };
39
+ export interface PanelImperativeHandle {
40
+ collapse(): void;
41
+ expand(): void;
42
+ getSize(): PanelSize;
43
+ isCollapsed(): boolean;
44
+ resize(size: number | string): void;
45
+ }
46
+ type DivStyle = Exclude<Octane.JSX.IntrinsicElements['div']['style'], string | undefined>;
47
+ type DivProps = Omit<
48
+ Octane.JSX.IntrinsicElements['div'],
49
+ 'children' | 'id' | 'ref' | 'onResize' | 'style'
50
+ >;
51
+ export type PanelProps = DivProps & {
52
+ children?: OctaneNode;
53
+ collapsedSize?: number | string;
54
+ collapsible?: boolean;
55
+ defaultSize?: number | string;
56
+ disabled?: boolean;
57
+ elementRef?: Octane.Ref<HTMLDivElement | null>;
58
+ groupResizeBehavior?: GroupResizeBehavior;
59
+ id?: string | number;
60
+ maxSize?: number | string;
61
+ minSize?: number | string;
62
+ onResize?: PanelResizeCallback;
63
+ panelRef?: Octane.Ref<PanelImperativeHandle | null>;
64
+ style?: DivStyle;
65
+ };
66
+ export type OnPanelResize = PanelProps['onResize'];
@@ -0,0 +1,6 @@
1
+ import { useState } from 'octane';
2
+ import type { PanelImperativeHandle } from './types';
3
+
4
+ export function usePanelCallbackRef(slot?: symbol) {
5
+ return useState<PanelImperativeHandle | null>(null, slot);
6
+ }
@@ -0,0 +1,42 @@
1
+ import { useImperativeHandle, useRef } from 'octane';
2
+ import type { Octane } from 'octane/jsx-runtime';
3
+ import { subSlot } from '../../internal';
4
+ import { getImperativePanelMethods } from '../../global/utils/getImperativePanelMethods';
5
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
6
+ import { useMergedRefs } from '../../hooks/useMergedRefs';
7
+ import { useGroupContext } from '../group/useGroupContext';
8
+ import type { PanelImperativeHandle } from './types';
9
+
10
+ const noop = () => {};
11
+
12
+ export function usePanelImperativeHandle(
13
+ panelId: string,
14
+ panelRef: Octane.Ref<PanelImperativeHandle | null> | undefined,
15
+ slot?: symbol,
16
+ ) {
17
+ const { id: groupId } = useGroupContext();
18
+ const imperativePanelRef = useRef<PanelImperativeHandle>(
19
+ {
20
+ collapse: noop,
21
+ expand: noop,
22
+ getSize: () => ({ asPercentage: 0, inPixels: 0 }),
23
+ isCollapsed: () => false,
24
+ resize: noop,
25
+ },
26
+ subSlot(slot, 'ref'),
27
+ );
28
+ const mergedPanelRef = useMergedRefs(panelRef, subSlot(slot, 'merged-ref'));
29
+ useImperativeHandle(
30
+ mergedPanelRef,
31
+ () => imperativePanelRef.current,
32
+ [],
33
+ subSlot(slot, 'handle'),
34
+ );
35
+ useIsomorphicLayoutEffect(
36
+ () => {
37
+ Object.assign(imperativePanelRef.current, getImperativePanelMethods({ groupId, panelId }));
38
+ },
39
+ [groupId, panelId],
40
+ subSlot(slot, 'effect'),
41
+ );
42
+ }
@@ -0,0 +1,6 @@
1
+ import { useRef } from 'octane';
2
+ import type { PanelImperativeHandle } from './types';
3
+
4
+ export function usePanelRef(slot?: symbol) {
5
+ return useRef<PanelImperativeHandle | null>(null, slot);
6
+ }
@@ -0,0 +1,126 @@
1
+ import { useEffect, useRef, useState } from 'octane';
2
+ import { subscribeToMountedGroup } from '../../global/mutable-state/groups';
3
+ import { subscribeToInteractionState } from '../../global/mutable-state/interactions';
4
+ import type { InteractionState } from '../../global/mutable-state/types';
5
+ import { calculateSeparatorAriaValues } from '../../global/utils/calculateSeparatorAriaValues';
6
+ import { useId } from '../../hooks/useId';
7
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
8
+ import { useMergedRefs } from '../../hooks/useMergedRefs';
9
+ import { useStableObject } from '../../hooks/useStableObject';
10
+ import { useGroupContext } from '../group/useGroupContext';
11
+ import type { RegisteredSeparator, SeparatorProps } from './types';
12
+
13
+ type SeparatorAria = {
14
+ valueControls?: string;
15
+ valueMin?: number;
16
+ valueMax?: number;
17
+ valueNow?: number;
18
+ };
19
+
20
+ export function Separator(props: SeparatorProps) @{
21
+ const {
22
+ children,
23
+ className,
24
+ disabled,
25
+ disableDoubleClick,
26
+ elementRef: elementRefProp,
27
+ id: idProp,
28
+ style,
29
+ ...rest
30
+ } = props;
31
+ const id = useId(idProp);
32
+ const stableProps = useStableObject({ disabled, disableDoubleClick });
33
+ const [aria, setAria] = useState<SeparatorAria>({});
34
+ const [dragState, setDragState] = useState<InteractionState['state']>('inactive');
35
+ const [isFocused, setIsFocused] = useState(false);
36
+ const elementRef = useRef<HTMLDivElement | null>(null);
37
+ const mergedRef = useMergedRefs(elementRef, elementRefProp);
38
+ const {
39
+ disableCursor,
40
+ id: groupId,
41
+ orientation: groupOrientation,
42
+ registerSeparator,
43
+ updateSeparatorProps,
44
+ } = useGroupContext();
45
+ const orientation =
46
+ groupOrientation === 'horizontal' ? 'vertical' : 'horizontal';
47
+
48
+ useIsomorphicLayoutEffect(() => {
49
+ const element = elementRef.current;
50
+ if (element === null) return;
51
+ const separator: RegisteredSeparator = {
52
+ disabled: stableProps.disabled,
53
+ disableDoubleClick: stableProps.disableDoubleClick,
54
+ element,
55
+ id,
56
+ };
57
+ const unregisterSeparator = registerSeparator(separator);
58
+ const removeInteractionListener = subscribeToInteractionState((event) => {
59
+ setDragState(
60
+ event.next.state !== 'inactive' &&
61
+ event.next.hitRegions.some((region) => region.separator === separator)
62
+ ? event.next.state
63
+ : 'inactive',
64
+ );
65
+ });
66
+ const removeGroupListener = subscribeToMountedGroup(groupId, (event) => {
67
+ const { derivedPanelConstraints, layout, separatorToPanels } = event.next;
68
+ const panels = separatorToPanels.get(separator);
69
+ if (!panels) return;
70
+ const primaryPanel = panels[0];
71
+ if (!primaryPanel) return;
72
+ setAria(
73
+ calculateSeparatorAriaValues({
74
+ layout,
75
+ panelConstraints: derivedPanelConstraints,
76
+ panelId: primaryPanel.id,
77
+ panelIndex: panels.indexOf(primaryPanel),
78
+ }),
79
+ );
80
+ });
81
+ return () => {
82
+ removeInteractionListener();
83
+ removeGroupListener();
84
+ unregisterSeparator();
85
+ };
86
+ }, [groupId, id, registerSeparator, stableProps]);
87
+
88
+ useEffect(() => updateSeparatorProps(id, { disabled, disableDoubleClick }), [
89
+ disabled,
90
+ disableDoubleClick,
91
+ id,
92
+ updateSeparatorProps,
93
+ ]);
94
+ const cursor =
95
+ disabled && !disableCursor ? 'not-allowed' : undefined;
96
+ const dataSeparator =
97
+ disabled
98
+ ? 'disabled'
99
+ : dragState === 'active'
100
+ ? 'active'
101
+ : isFocused
102
+ ? 'focus'
103
+ : dragState;
104
+
105
+ return <div
106
+ {...rest}
107
+ aria-controls={aria.valueControls}
108
+ aria-disabled={disabled || undefined}
109
+ aria-orientation={orientation}
110
+ aria-valuemax={aria.valueMax}
111
+ aria-valuemin={aria.valueMin}
112
+ aria-valuenow={aria.valueNow}
113
+ className={className}
114
+ data-separator={dataSeparator}
115
+ data-testid={id}
116
+ id={id}
117
+ onBlur={() => setIsFocused(false)}
118
+ onFocus={() => setIsFocused(true)}
119
+ ref={mergedRef}
120
+ role="separator"
121
+ style={{ flexBasis: 'auto', cursor, ...style, flexGrow: 0, flexShrink: 0, touchAction: 'none' }}
122
+ tabIndex={disabled ? undefined : 0}
123
+ >{children}</div>;
124
+ }
125
+
126
+ (Separator as { displayName?: string }).displayName = 'Separator';
@@ -0,0 +1,22 @@
1
+ import type { OctaneNode } from 'octane';
2
+ import type { Octane } from 'octane/jsx-runtime';
3
+
4
+ type DivStyle = Exclude<Octane.JSX.IntrinsicElements['div']['style'], string | undefined>;
5
+ type DivProps = Omit<
6
+ Octane.JSX.IntrinsicElements['div'],
7
+ 'children' | 'id' | 'ref' | 'role' | 'tabIndex' | 'style'
8
+ >;
9
+ export type SeparatorProps = DivProps & {
10
+ children?: OctaneNode;
11
+ disabled?: boolean;
12
+ disableDoubleClick?: boolean;
13
+ elementRef?: Octane.Ref<HTMLDivElement | null>;
14
+ id?: string | number;
15
+ style?: DivStyle;
16
+ };
17
+ export type RegisteredSeparator = {
18
+ disabled?: boolean;
19
+ disableDoubleClick?: boolean;
20
+ element: HTMLDivElement;
21
+ id: string;
22
+ };
@@ -0,0 +1,25 @@
1
+ // Constants used for memoization
2
+ export const EMPTY_ARRAY: unknown[] = [];
3
+ export const EMPTY_DOM_RECT: DOMRectReadOnly = {
4
+ bottom: 0,
5
+ height: 0,
6
+ left: 0,
7
+ right: 0,
8
+ toJSON: () => {},
9
+ top: 0,
10
+ width: 0,
11
+ x: 0,
12
+ y: 0,
13
+ };
14
+ export const EMPTY_OBJECT = {};
15
+ export const EMPTY_POINT = { x: 0, y: 0 };
16
+ export const IDENTITY_FUNCTION = <Value>(value: Value) => value;
17
+ export const NOOP_FUNCTION = () => {};
18
+
19
+ // Cursor flags
20
+ export const CURSOR_FLAG_HORIZONTAL_MIN = 0b0001;
21
+ export const CURSOR_FLAG_HORIZONTAL_MAX = 0b0010;
22
+ export const CURSOR_FLAG_VERTICAL_MIN = 0b0100;
23
+ export const CURSOR_FLAG_VERTICAL_MAX = 0b1000;
24
+ export const CURSOR_FLAGS_HORIZONTAL = 0b0011;
25
+ export const CURSOR_FLAGS_VERTICAL = 0b1100;