Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/apollo-wind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"next-themes": "^0.4.6",
"react-day-picker": "^9.13.0",
"react-hook-form": "^7.66.1",
"react-resizable-panels": "^3.0.6",
"react-resizable-panels": "^4.6.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
Expand Down
38 changes: 19 additions & 19 deletions packages/apollo-wind/src/components/ui/resizable.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export const Horizontal = {
args: {},
render: () => (
<div className="h-screen p-4">
<ResizablePanelGroup direction="horizontal" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize={50} minSize={20}>
<ResizablePanelGroup orientation="horizontal" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize="50%" minSize="20%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Left Panel</span>
</Row>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50} minSize={20}>
<ResizablePanel defaultSize="50%" minSize="20%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Right Panel</span>
</Row>
Expand All @@ -38,14 +38,14 @@ export const Vertical = {
args: {},
render: () => (
<div className="h-screen p-4">
<ResizablePanelGroup direction="vertical" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize={50} minSize={20}>
<ResizablePanelGroup orientation="vertical" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize="50%" minSize="20%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Top Panel</span>
</Row>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50} minSize={20}>
<ResizablePanel defaultSize="50%" minSize="20%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Bottom Panel</span>
</Row>
Expand All @@ -59,20 +59,20 @@ export const ThreePanels = {
args: {},
render: () => (
<div className="h-screen p-4">
<ResizablePanelGroup direction="horizontal" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize={25} minSize={15}>
<ResizablePanelGroup orientation="horizontal" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize="25%" minSize="15%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Sidebar</span>
</Row>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50} minSize={30}>
<ResizablePanel defaultSize="50%" minSize="30%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Main Content</span>
</Row>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={25} minSize={15}>
<ResizablePanel defaultSize="25%" minSize="15%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Right Sidebar</span>
</Row>
Expand All @@ -86,22 +86,22 @@ export const Nested = {
args: {},
render: () => (
<div className="h-screen p-4">
<ResizablePanelGroup direction="horizontal" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize={25} minSize={15}>
<ResizablePanelGroup orientation="horizontal" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize="25%" minSize="15%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Left Sidebar</span>
</Row>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={50} minSize={30}>
<ResizablePanel defaultSize="75%">
<ResizablePanelGroup orientation="vertical">
<ResizablePanel defaultSize="50%" minSize="30%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Top Content</span>
</Row>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50} minSize={30}>
<ResizablePanel defaultSize="50%" minSize="30%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Bottom Content</span>
</Row>
Expand All @@ -117,14 +117,14 @@ export const WithHandleVariant = {
args: {},
render: () => (
<div className="h-screen p-4">
<ResizablePanelGroup direction="horizontal" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize={50} minSize={20}>
<ResizablePanelGroup orientation="horizontal" className="min-h-[400px] rounded-lg border">
<ResizablePanel defaultSize="50%" minSize="20%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Left Panel</span>
</Row>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50} minSize={20}>
<ResizablePanel defaultSize="50%" minSize="20%">
<Row h="full" align="center" justify="center" className="p-6">
<span className="font-semibold">Right Panel</span>
</Row>
Expand Down
68 changes: 31 additions & 37 deletions packages/apollo-wind/src/components/ui/resizable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { describe, expect, it } from 'vitest';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './resizable';

const BasicResizable = ({ withHandle = false }: { withHandle?: boolean }) => (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={50}>
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="50%">
<div>Panel 1</div>
</ResizablePanel>
<ResizableHandle withHandle={withHandle} />
<ResizablePanel defaultSize={50}>
<ResizablePanel defaultSize="50%">
<div>Panel 2</div>
</ResizablePanel>
</ResizablePanelGroup>
Expand All @@ -26,7 +26,7 @@ describe('Resizable', () => {

it('renders handle without grip icon by default', () => {
const { container } = render(<BasicResizable />);
expect(container.querySelector('[data-panel-resize-handle-id]')).toBeInTheDocument();
expect(container.querySelector('[data-separator]')).toBeInTheDocument();
expect(container.querySelector('svg')).not.toBeInTheDocument();
});

Expand All @@ -37,46 +37,40 @@ describe('Resizable', () => {

it('renders vertical panel group', () => {
const { container } = render(
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={50}>
<ResizablePanelGroup orientation="vertical">
<ResizablePanel defaultSize="50%">
<div>Top</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanel defaultSize="50%">
<div>Bottom</div>
</ResizablePanel>
</ResizablePanelGroup>
);

expect(
container.querySelector("[data-panel-group-direction='vertical']")
).toBeInTheDocument();
expect(container.querySelector('[data-group]')).toBeInTheDocument();
});
});

describe('Accessibility', () => {
// Note: react-resizable-panels has a known a11y issue - missing aria-valuenow on slider role
// This should be fixed upstream in the library
it('has accessibility violation for missing aria-valuenow (known library issue)', async () => {
it('has no accessibility violations', async () => {
const { container } = render(<BasicResizable />);
const results = await axe(container);
// The library doesn't provide aria-valuenow which is required for slider role
expect(results.violations.length).toBeGreaterThan(0);
expect(results.violations[0].id).toBe('aria-required-attr');
expect(results.violations).toHaveLength(0);
});

it('handle is keyboard focusable', () => {
const { container } = render(<BasicResizable />);
const handle = container.querySelector('[data-panel-resize-handle-id]');
const handle = container.querySelector('[data-separator]');
expect(handle).toHaveAttribute('tabindex', '0');
});
});

describe('Props', () => {
it('applies custom className to panel group', () => {
const { container } = render(
<ResizablePanelGroup direction="horizontal" className="custom-group">
<ResizablePanel defaultSize={100}>
<ResizablePanelGroup orientation="horizontal" className="custom-group">
<ResizablePanel defaultSize="100%">
<div>Content</div>
</ResizablePanel>
</ResizablePanelGroup>
Expand All @@ -87,28 +81,28 @@ describe('Resizable', () => {

it('applies custom className to handle', () => {
const { container } = render(
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={50}>
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="50%">
<div>Panel 1</div>
</ResizablePanel>
<ResizableHandle className="custom-handle" />
<ResizablePanel defaultSize={50}>
<ResizablePanel defaultSize="50%">
<div>Panel 2</div>
</ResizablePanel>
</ResizablePanelGroup>
);

expect(container.querySelector('[data-panel-resize-handle-id]')).toHaveClass('custom-handle');
expect(container.querySelector('[data-separator]')).toHaveClass('custom-handle');
});

it('renders panel with minSize', () => {
render(
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={50} minSize={25}>
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="50%" minSize="25%">
<div>Panel 1</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanel defaultSize="50%">
<div>Panel 2</div>
</ResizablePanel>
</ResizablePanelGroup>
Expand All @@ -119,12 +113,12 @@ describe('Resizable', () => {

it('renders panel with maxSize', () => {
render(
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={50} maxSize={75}>
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="50%" maxSize="75%">
<div>Panel 1</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanel defaultSize="50%">
<div>Panel 2</div>
</ResizablePanel>
</ResizablePanelGroup>
Expand All @@ -135,12 +129,12 @@ describe('Resizable', () => {

it('renders collapsible panel', () => {
render(
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={50} collapsible collapsedSize={0}>
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="50%" collapsible collapsedSize="0%">
<div>Collapsible</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanel defaultSize="50%">
<div>Panel 2</div>
</ResizablePanel>
</ResizablePanelGroup>
Expand All @@ -153,16 +147,16 @@ describe('Resizable', () => {
describe('Multiple Panels', () => {
it('renders three panels with two handles', () => {
const { container } = render(
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={33}>
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="33%">
<div>Panel 1</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={34}>
<ResizablePanel defaultSize="34%">
<div>Panel 2</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={33}>
<ResizablePanel defaultSize="33%">
<div>Panel 3</div>
</ResizablePanel>
</ResizablePanelGroup>
Expand All @@ -171,7 +165,7 @@ describe('Resizable', () => {
expect(screen.getByText('Panel 1')).toBeInTheDocument();
expect(screen.getByText('Panel 2')).toBeInTheDocument();
expect(screen.getByText('Panel 3')).toBeInTheDocument();
expect(container.querySelectorAll('[data-panel-resize-handle-id]')).toHaveLength(2);
expect(container.querySelectorAll('[data-separator]')).toHaveLength(2);
});
});
});
26 changes: 11 additions & 15 deletions packages/apollo-wind/src/components/ui/resizable.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,36 @@
import { GripVertical } from 'lucide-react';
import * as ResizablePrimitive from 'react-resizable-panels';
import { Group, Panel, Separator } from 'react-resizable-panels';
import type { PanelImperativeHandle } from 'react-resizable-panels';

import { cn } from '@/lib/index';

const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
{...props}
/>
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof Group>) => (
<Group className={cn('flex h-full w-full', className)} {...props} />
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The ResizablePanelGroup wrapper removed the flex-direction styling that was present in v3 (data-[panel-group-direction=vertical]:flex-col). In v4, the Group component may not automatically apply flex-col for vertical orientation. This should be replaced with aria-[orientation=vertical]:flex-col to ensure vertical panel groups render correctly with a column layout instead of defaulting to row layout.

Suggested change
<Group className={cn('flex h-full w-full', className)} {...props} />
<Group className={cn('flex h-full w-full aria-[orientation=vertical]:flex-col', className)} {...props} />

Copilot uses AI. Check for mistakes.
);

const ResizablePanel = ResizablePrimitive.Panel;
const ResizablePanel = Panel;

const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
}: React.ComponentProps<typeof Separator> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
<Separator
className={cn(
'group relative flex w-px items-center justify-center bg-border transition-colors data-[resize-handle-state=hover]:bg-primary data-[resize-handle-state=drag]:bg-primary after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
'group relative flex w-px items-center justify-center bg-border transition-colors data-[separator=hover]:bg-primary data-[separator=active]:bg-primary after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:-translate-y-1/2 aria-[orientation=horizontal]:after:translate-x-0 [&[aria-orientation=horizontal]>div]:rotate-90',
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border transition-colors group-data-[resize-handle-state=hover]:bg-primary group-data-[resize-handle-state=drag]:bg-primary">
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border transition-colors group-data-[separator=hover]:bg-primary group-data-[separator=active]:bg-primary">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
</Separator>
);

export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
export type { PanelImperativeHandle };
Loading
Loading