Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions src/components/ui/Splitter/Splitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,22 @@ import SplitterHandle from './fragments/SplitterHandle';
* ```
*/

// Empty props type - only supporting fragment exports
export type SplitterProps = React.HTMLAttributes<HTMLDivElement> & {
children?: React.ReactNode;
export type SplitterProps = React.ComponentPropsWithoutRef<'div'>;

type SplitterComponent = React.ForwardRefExoticComponent<
SplitterProps & React.RefAttributes<HTMLDivElement>
> & {
Root: typeof SplitterRoot;
Panel: typeof SplitterPanel;
Handle: typeof SplitterHandle;
};

// Empty implementation - we don't support direct usage
const Splitter = () => {
const Splitter = React.forwardRef<React.ElementRef<'div'>, SplitterProps>((props, ref) => {
console.warn('Direct usage of Splitter is not supported. Please use Splitter.Root, Splitter.Panel, etc. instead.');
return null;
};
return <div ref={ref} {...props} />;
}) as SplitterComponent;

Splitter.displayName = 'Splitter';

Splitter.Root = SplitterRoot;
Splitter.Panel = SplitterPanel;
Expand Down
26 changes: 12 additions & 14 deletions src/components/ui/Splitter/fragments/SplitterHandle.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
'use client';
import React from 'react';
import { customClassSwitcher } from '~/core';
import { clsx } from 'clsx';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect clsx import.

clsx has a default export. Named import will break at runtime.

Apply this diff:

-import { clsx } from 'clsx';
+import clsx from 'clsx';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { clsx } from 'clsx';
import clsx from 'clsx';
🤖 Prompt for AI Agents
In src/components/ui/Splitter/fragments/SplitterHandle.tsx around line 3, the
code imports clsx using a named import which is incorrect because clsx uses a
default export; replace the named import with a default import (e.g., import
clsx from 'clsx') so runtime errors are avoided, and ensure any existing usages
of clsx in the file remain unchanged.

import { useSplitter } from './SplitterRoot';

export interface SplitterHandleProps {
export interface SplitterHandleProps extends React.ComponentPropsWithoutRef<'div'> {
index: number;
className?: string;
customRootClass?: string;
'aria-label'?: string;
}

const SplitterHandle: React.FC<SplitterHandleProps> = ({
index,
className = '',
customRootClass = '',
'aria-label': ariaLabel
}) => {
const SplitterHandle = React.forwardRef<
React.ElementRef<'div'>,
SplitterHandleProps
>(({ index, className, 'aria-label': ariaLabel, style, ...props }, forwardedRef) => {
const { startDrag, orientation, isDragging, activeHandleIndex, handleKeyDown, rootClass } = useSplitter();
const isHorizontal = orientation === 'horizontal';
const isActive = isDragging && activeHandleIndex === index;

return (
<div
className={`${rootClass}-handle`}
{...props}
ref={forwardedRef}
className={clsx(`${rootClass}-handle`, className)}
role="separator"
aria-orientation={orientation}
aria-label={ariaLabel || `${orientation} resize handle`}
tabIndex={0}
onMouseDown={(e) => startDrag(index, e)}
onTouchStart={(e) => startDrag(index, e)}
onKeyDown={(e) => handleKeyDown(index, e)}

style={style}
/>
Comment on lines +20 to 31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t overwrite consumer event handlers; compose them with internal ones.

Current {...props} order causes any passed onMouseDown/onTouchStart/onKeyDown to be silently overridden. Compose them so analytics and side-effects still fire.

Apply this diff:

->(({ index, className, 'aria-label': ariaLabel, style, ...props }, forwardedRef) => {
+>(({ index, className, 'aria-label': ariaLabel, style, onMouseDown: userOnMouseDown, onTouchStart: userOnTouchStart, onKeyDown: userOnKeyDown, ...props }, forwardedRef) => {
@@
-        <div
-            {...props}
-            ref={forwardedRef}
+        <div
+            {...props}
+            ref={forwardedRef}
             className={clsx(`${rootClass}-handle`, className)}
             role="separator"
             aria-orientation={orientation}
             aria-label={ariaLabel || `${orientation} resize handle`}
             tabIndex={0}
-            onMouseDown={(e) => startDrag(index, e)}
-            onTouchStart={(e) => startDrag(index, e)}
-            onKeyDown={(e) => handleKeyDown(index, e)}
+            onMouseDown={(e) => {
+                userOnMouseDown?.(e);
+                startDrag(index, e);
+            }}
+            onTouchStart={(e) => {
+                userOnTouchStart?.(e);
+                startDrag(index, e);
+            }}
+            onKeyDown={(e) => {
+                userOnKeyDown?.(e);
+                handleKeyDown(index, e);
+            }}
             style={style}
         />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{...props}
ref={forwardedRef}
className={clsx(`${rootClass}-handle`, className)}
role="separator"
aria-orientation={orientation}
aria-label={ariaLabel || `${orientation} resize handle`}
tabIndex={0}
onMouseDown={(e) => startDrag(index, e)}
onTouchStart={(e) => startDrag(index, e)}
onKeyDown={(e) => handleKeyDown(index, e)}
style={style}
/>
// File: src/components/ui/Splitter/fragments/SplitterHandle.tsx
// Lines: 20-31
(({
index,
className,
'aria-label': ariaLabel,
style,
onMouseDown: userOnMouseDown,
onTouchStart: userOnTouchStart,
onKeyDown: userOnKeyDown,
...props
}, forwardedRef) => {
return (
<div
{...props}
ref={forwardedRef}
className={clsx(`${rootClass}-handle`, className)}
role="separator"
aria-orientation={orientation}
aria-label={ariaLabel || `${orientation} resize handle`}
tabIndex={0}
onMouseDown={(e) => {
userOnMouseDown?.(e);
startDrag(index, e);
}}
onTouchStart={(e) => {
userOnTouchStart?.(e);
startDrag(index, e);
}}
onKeyDown={(e) => {
userOnKeyDown?.(e);
handleKeyDown(index, e);
}}
style={style}
/>
);
})
🤖 Prompt for AI Agents
In src/components/ui/Splitter/fragments/SplitterHandle.tsx around lines 20–31,
the spread {...props} followed by internal onMouseDown/onTouchStart/onKeyDown
handlers silently overrides consumer handlers; fix by composing the handlers
instead: read any existing props.onMouseDown/onTouchStart/onKeyDown into local
constants and create composed handlers that call the consumer handler (if
present) and then the internal startDrag/handleKeyDown (or the other order if
you prefer consumer first), passing the original event through and preserving
return values/stopPropagation behavior; replace the hardwired
onMouseDown/onTouchStart/onKeyDown props with these composed functions so
consumer analytics/side-effects still run.

);
};
});

SplitterHandle.displayName = 'SplitterHandle';

export default SplitterHandle;
35 changes: 19 additions & 16 deletions src/components/ui/Splitter/fragments/SplitterPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,43 @@
'use client';
import React, { ReactNode } from 'react';
import { customClassSwitcher } from '~/core';
import React from 'react';
import { clsx } from 'clsx';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect clsx import.

Use default import to avoid runtime failure.

Apply this diff:

-import { clsx } from 'clsx';
+import clsx from 'clsx';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { clsx } from 'clsx';
import clsx from 'clsx';
🤖 Prompt for AI Agents
In src/components/ui/Splitter/fragments/SplitterPanel.tsx around line 3, the
file currently imports clsx as a named import which causes a runtime failure;
change the import to use the default export from 'clsx' (replace the named
import with a default import) so existing usages of clsx work at runtime.

import { useSplitter } from './SplitterRoot';

export interface SplitterPanelProps {
export interface SplitterPanelProps extends React.ComponentPropsWithoutRef<'div'> {
index: number;
children: ReactNode;
className?: string;
customRootClass?: string;
minSize?: number;
maxSize?: number;
}

const SplitterPanel: React.FC<SplitterPanelProps> = ({
index,
children,
className = '',
customRootClass = ''
}) => {
const SplitterPanel = React.forwardRef<
React.ElementRef<'div'>,
SplitterPanelProps
>(({ index, children, className, style, ...props }, forwardedRef) => {
const { sizes, orientation, rootClass } = useSplitter();

const style = {
const panelStyle = {
flexBasis: `${sizes[index]}%`,
flexGrow: 0,
flexShrink: 0,
overflow: 'auto',
minWidth: orientation === 'horizontal' ? 0 : undefined,
minHeight: orientation === 'vertical' ? 0 : undefined
};
minHeight: orientation === 'vertical' ? 0 : undefined,
...style
} as React.CSSProperties;
Comment on lines +19 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard against undefined sizes[index] to avoid “undefined%”.

If sizes[index] is missing, flexBasis becomes "undefined%". Guard and only set when defined.

Apply this diff:

-    const panelStyle = {
-        flexBasis: `${sizes[index]}%`,
+    const basis = sizes[index];
+    const panelStyle: React.CSSProperties = {
+        ...(typeof basis === 'number' ? { flexBasis: `${basis}%` } : {}),
         flexGrow: 0,
         flexShrink: 0,
         overflow: 'auto',
         minWidth: orientation === 'horizontal' ? 0 : undefined,
-        minHeight: orientation === 'vertical' ? 0 : undefined,
-        ...style
-    } as React.CSSProperties;
+        minHeight: orientation === 'vertical' ? 0 : undefined,
+        ...style
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const panelStyle = {
flexBasis: `${sizes[index]}%`,
flexGrow: 0,
flexShrink: 0,
overflow: 'auto',
minWidth: orientation === 'horizontal' ? 0 : undefined,
minHeight: orientation === 'vertical' ? 0 : undefined
};
minHeight: orientation === 'vertical' ? 0 : undefined,
...style
} as React.CSSProperties;
const basis = sizes[index];
const panelStyle: React.CSSProperties = {
...(typeof basis === 'number' ? { flexBasis: `${basis}%` } : {}),
flexGrow: 0,
flexShrink: 0,
overflow: 'auto',
minWidth: orientation === 'horizontal' ? 0 : undefined,
minHeight: orientation === 'vertical' ? 0 : undefined,
...style
};
🤖 Prompt for AI Agents
In src/components/ui/Splitter/fragments/SplitterPanel.tsx around lines 19 to 27,
the panelStyle builds flexBasis using `${sizes[index]}%` which yields
"undefined%" when sizes[index] is missing; change the logic to only set
flexBasis when sizes[index] is a defined number (e.g., compute a variable like
const flexBasis = typeof sizes[index] === 'number' ? `${sizes[index]}%` :
undefined and include flexBasis in the style object conditionally) so that
flexBasis is omitted when the size is undefined, preserving types as
React.CSSProperties.


return (
<div className={`${rootClass}-panel`} style={style}>
<div
{...props}
ref={forwardedRef}
className={clsx(`${rootClass}-panel`, className)}
style={panelStyle}
>
{children}
</div>
);
};
});

SplitterPanel.displayName = 'SplitterPanel';

export default SplitterPanel;
40 changes: 28 additions & 12 deletions src/components/ui/Splitter/fragments/SplitterRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
'use client';
import React, { useState, useRef, useCallback, ReactNode, useMemo, useEffect } from 'react';
import React, { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { customClassSwitcher } from '~/core';
import { clsx } from 'clsx';
import SplitterContext, { SplitterContextValue, SplitterOrientation } from '../context/SplitterContext';

export interface SplitterRootProps {
export interface SplitterRootProps extends React.ComponentPropsWithoutRef<'div'> {
orientation?: SplitterOrientation;
children: ReactNode;
className?: string;
customRootClass?: string;
defaultSizes?: number[];
minSizes?: number[];
Expand All @@ -26,23 +24,37 @@ export const useSplitter = () => {

const COMPONENT_NAME = 'Splitter';

const SplitterRoot: React.FC<SplitterRootProps> = ({
const SplitterRoot = React.forwardRef<
React.ElementRef<'div'>,
SplitterRootProps
>(({
orientation = 'horizontal',
children,
className = '',
className,
customRootClass = '',
defaultSizes = [50, 50],
minSizes = [0, 0],
maxSizes = [100, 100],
onSizesChange
}) => {
onSizesChange,
style,
...props
}, forwardedRef) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
const containerRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [sizes, setSizes] = useState<number[]>(defaultSizes);
const [isDragging, setIsDragging] = useState(false);
const [activeHandleIndex, setActiveHandleIndex] = useState<number | null>(null);
const [dragStart, setDragStart] = useState<{ position: number; sizes: number[] } | null>(null);

const mergedRef = useCallback((node: HTMLDivElement | null) => {
containerRef.current = node;
if (typeof forwardedRef === 'function') {
forwardedRef(node);
} else if (forwardedRef) {
(forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
}
}, [forwardedRef]);

// Performance optimization: Memoize constraints to prevent unnecessary recalculations
const constraints = useMemo(() => ({
minSizes: minSizes || [],
Expand Down Expand Up @@ -293,19 +305,23 @@ const SplitterRoot: React.FC<SplitterRootProps> = ({
return (
<SplitterContext.Provider value={contextValue}>
<div
ref={containerRef}
{...props}
ref={mergedRef}
className={clsx(rootClass, className)}
style={{
display: 'flex',
flexDirection: isHorizontal ? 'row' : 'column',
width: '100%',
height: '100%'
height: '100%',
...style
}}
>
{children}
</div>
</SplitterContext.Provider>
);
};
});

SplitterRoot.displayName = 'SplitterRoot';

export default SplitterRoot;
24 changes: 24 additions & 0 deletions src/components/ui/Splitter/tests/Splitter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,28 @@ describe('Splitter Component', () => {

consoleSpy.mockRestore();
});

it('forwards refs to DOM elements', () => {
const rootRef = React.createRef<HTMLDivElement>();
const panelRef = React.createRef<HTMLDivElement>();
const handleRef = React.createRef<HTMLDivElement>();

render(
<div style={{ width: '400px', height: '300px' }}>
<Splitter.Root ref={rootRef}>
<Splitter.Panel index={0} ref={panelRef}>
<div data-testid="panel-0">Panel 0</div>
</Splitter.Panel>
<Splitter.Handle index={0} ref={handleRef} />
<Splitter.Panel index={1}>
<div data-testid="panel-1">Panel 1</div>
</Splitter.Panel>
</Splitter.Root>
</div>
);

expect(rootRef.current).toBeInstanceOf(HTMLDivElement);
expect(panelRef.current).toBeInstanceOf(HTMLDivElement);
expect(handleRef.current).toBeInstanceOf(HTMLDivElement);
});
});
Loading