Skip to content

Commit

Permalink
feat(toolbar): add toolbar item and clear action
Browse files Browse the repository at this point in the history
  • Loading branch information
Tonours committed Feb 7, 2024
1 parent 77d9d77 commit c7d034d
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 55 deletions.
13 changes: 13 additions & 0 deletions app/components/ui/Toolbar/Toolbar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@ describe('Toolbar', () => {
expect(screen.getByTestId('toolbar')).toHaveClass(customClass);
});
});

describe('ToolbarItem', () => {
it('renders children correctly', () => {
render(<Toolbar.Item>Hello World</Toolbar.Item>);
expect(screen.getByTestId('toolbar-item')).toBeInTheDocument();
expect(screen.getByTestId('toolbar-item')).toHaveTextContent('Hello World');
});

it('applies custom className', () => {
render(<Toolbar.Item className="custom-class">Content</Toolbar.Item>);
expect(screen.getByTestId('toolbar-item')).toHaveClass('custom-class');
});
});
22 changes: 21 additions & 1 deletion app/components/ui/Toolbar/Toolbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Toolbar } from './Toolbar';
import IconCircle from '~/icons/icon-circle.svg?react';
import IconTrash from '~/icons/icon-trash.svg?react';

const meta = {
title: 'UI/Toolbar',
Expand All @@ -23,4 +25,22 @@ export const Default: Story = {
args: {
children: 'Default Toolbar',
},
};
};

export const WithItems: Story = {
args: {
children: (
<>
<Toolbar.Item>
<IconCircle className="w-[6px] h-[6px]" />
</Toolbar.Item>
<Toolbar.Item>
<IconCircle className="w-[10px] h-[10px]" />
</Toolbar.Item>
<Toolbar.Item className="col-span-2">
<IconTrash className="w-3 h-[auto]" />
</Toolbar.Item>
</>
),
},
};
29 changes: 27 additions & 2 deletions app/components/ui/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const Toolbar = ({ className, children }: ToolbarProps) => {
});

const classNames = clsx(
'flex flex-col cursor-auto relative w-[120px] min-h-[200px] bg-white border-gray-100 border p-4 rounded-lg drop-shadow-sm transition-opacity duration-200 translate-x-4 translate-y-4 z-50',
'flex flex-col cursor-auto relative w-[120px] min-h-[200px] bg-white border-gray-100 border p-2 rounded-lg drop-shadow-sm transition-opacity duration-200 translate-x-4 translate-y-4 z-50',
className,
{
'opacity-50': isDragging,
Expand All @@ -36,7 +36,7 @@ export const Toolbar = ({ className, children }: ToolbarProps) => {
data-testid="toolbar"
tabIndex={-1}
>
{children}
<ul className="grid grid-cols-2 gap-2">{children}</ul>

<button
data-testid="toolbar-drag-handle"
Expand All @@ -55,3 +55,28 @@ export const Toolbar = ({ className, children }: ToolbarProps) => {
</>
);
};

export type ToolbarItemProps = {
children: React.ReactNode;
className?: string;
onClick?: (e: React.MouseEvent<HTMLElement>) => void;
};

export const ToolbarItem = ({
children,
className,
onClick,
}: ToolbarItemProps) => {
const classNames = clsx(
'w-full flex items-center justify-center p-2 bg-gray-100 text-gray-700 hover:text-gray-900 hover:bg-gray-200 rounded-sm duration-150',
className
);

return (
<button onClick={onClick} className={classNames} data-testid="toolbar-item">
{children}
</button>
);
};

Toolbar.Item = ToolbarItem;
7 changes: 2 additions & 5 deletions app/components/ui/WhiteBoard/WhiteBoard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import clsx from 'clsx';
import { useCanvasDrawing } from '~/hooks/useCanvasDrawing';

type WhiteBoardProps = {
className?: string;
parentRef: React.RefObject<HTMLDivElement>;
canvasRef: React.RefObject<HTMLCanvasElement>;
};

export const WhiteBoard = ({ className, parentRef }: WhiteBoardProps) => {
const [canvasRef] = useCanvasDrawing(parentRef);

export const WhiteBoard = ({ className, canvasRef }: WhiteBoardProps) => {
return (
<canvas
ref={canvasRef}
Expand Down
101 changes: 60 additions & 41 deletions app/hooks/useCanvasDrawing.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,108 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useCallback } from 'react';

type Line = { x: number; y: number }[];

export const useCanvasDrawing = (
parentRef?: React.RefObject<HTMLDivElement>
) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [lines, setLines] = useState<Line[]>([]);
const isDrawingRef = useRef(false);
const linesRef = useRef<Line[]>([]);

useEffect(() => {
const canvas = canvasRef.current;
const clearCanvas = () => {
linesRef.current = [];
redrawLines();
};

const drawLine = useCallback((line: Line) => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const ctx = canvas.getContext('2d');

if (!ctx || line.length < 2) {
return;
}

const [start, ...rest] = line;
ctx.beginPath();
ctx.moveTo(start.x, start.y);
rest.forEach((point) => {
ctx.lineTo(point.x, point.y);
});
ctx.stroke();
}, []);

const redrawLines = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
if (!canvas || !ctx) {
return;
}

const redrawLines = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
lines.forEach((line) => {
ctx.beginPath();
line.forEach((point, index) => {
if (index === 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
ctx.stroke();
}
});
ctx.closePath();
});
};
ctx.clearRect(0, 0, canvas.width, canvas.height);
linesRef.current.forEach(drawLine);
}, [drawLine]);

const resizeCanvas = () => {
canvas.width = parentRef?.current?.clientWidth ?? window.innerWidth;
canvas.height = parentRef?.current?.clientHeight ?? window.innerHeight;
redrawLines();
};
// Gestion du redimensionnement avec debounce pour optimiser les performances
const resizeCanvas = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas || !parentRef?.current) return;

window.addEventListener('resize', resizeCanvas);
resizeCanvas();
canvas.width = parentRef.current.clientWidth;
canvas.height = parentRef.current.clientHeight;
redrawLines();
}, [redrawLines, parentRef]);

const startDrawing = (e: MouseEvent) => {
setIsDrawing(true);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;

// Ajout des écouteurs d'événements
const startDrawing = (e: MouseEvent) => {
isDrawingRef.current = true;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setLines((prevLines) => [...prevLines, [{ x, y }]]);
linesRef.current.push([{ x, y }]);
};

const draw = (e: MouseEvent) => {
if (!isDrawing) return;
const currentLine = lines[lines.length - 1];
if (!isDrawingRef.current) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const newLine = [...currentLine, { x, y }];

setLines((prevLines) => [...prevLines.slice(0, -1), newLine]);
redrawLines();
const currentLine = linesRef.current[linesRef.current.length - 1];
currentLine.push({ x, y });
drawLine(currentLine);
};

const stopDrawing = () => {
setIsDrawing(false);
isDrawingRef.current = false;
redrawLines(); // Redessine pour s'assurer que tout est cohérent
};

canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseleave', stopDrawing);

window.addEventListener('resize', resizeCanvas);
resizeCanvas(); // Applique initialement la taille du canvas

// Nettoyage des écouteurs d'événements
return () => {
window.removeEventListener('resize', resizeCanvas);
canvas.removeEventListener('mousedown', startDrawing);
canvas.removeEventListener('mousemove', draw);
canvas.removeEventListener('mouseup', stopDrawing);
canvas.removeEventListener('mouseleave', stopDrawing);
window.removeEventListener('resize', resizeCanvas);
};
}, [parentRef, lines, isDrawing]);
}, [resizeCanvas, drawLine, redrawLines]);

return [canvasRef, lines] as const;
return { canvasRef, clearCanvas };
};
1 change: 1 addition & 0 deletions app/icons/icon-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions app/icons/icon-trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 12 additions & 6 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Toolbar } from '~/components/ui/Toolbar';
import { WhiteBoard } from '~/components/ui/WhiteBoard';
import { useRef } from 'react';
import { ClientRect, DndContext, Modifier } from '@dnd-kit/core';
import { restrictToParentElement } from '@dnd-kit/modifiers';

import MainLayout from '~/layouts/_main';
import { useRef } from 'react';
import { restrictToParentElement } from '@dnd-kit/modifiers';
import { Toolbar } from '~/components/ui/Toolbar';
import { WhiteBoard } from '~/components/ui/WhiteBoard';
import { snapBottomToCursor } from '~/utils/dndkit';

import IconTrash from '~/icons/icon-trash.svg?react';
import { useCanvasDrawing } from '~/hooks/useCanvasDrawing';

export default function Index() {
const parentRef = useRef<HTMLDivElement | null>(null);
const { canvasRef, clearCanvas } = useCanvasDrawing(parentRef);
const modifiers = [
snapBottomToCursor,
(e: Parameters<Modifier>[0]) =>
Expand All @@ -23,10 +27,12 @@ export default function Index() {
<div ref={parentRef} className="bg-gray-50 w-full h-full relative">
<DndContext modifiers={modifiers}>
<Toolbar>
<strong>WIP</strong>
<Toolbar.Item className="col-span-2" onClick={() => clearCanvas()}>
<IconTrash className="w-3 h-[auto]" />
</Toolbar.Item>
</Toolbar>

<WhiteBoard parentRef={parentRef} />
<WhiteBoard canvasRef={canvasRef} />
</DndContext>
</div>
</MainLayout>
Expand Down

0 comments on commit c7d034d

Please sign in to comment.