-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GUI menus and basic layout. #102
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\ | ||
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause. | ||
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md | ||
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ | ||
|
||
import { FC } from "react"; | ||
import { AiOutlinePython as PythonIcon } from "react-icons/ai"; | ||
import { | ||
FiActivity as ActivityIcon, | ||
FiDatabase as DatabaseIcon, | ||
FiSettings as SettingsIcon, | ||
FiSliders as SlidersIcon, | ||
FiTerminal as TerminalIcon, | ||
} from "react-icons/fi"; | ||
|
||
import { Menu } from "~/components/Menu"; | ||
import { PythonShell } from "~/components/PythonShell"; | ||
import { Viewer } from "~/components/Viewer"; | ||
|
||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
export const App: FC = () => { | ||
const leftIconSize = 24; | ||
const bottomIconSize = 16; | ||
return ( | ||
<div className="h-screen w-screen flex flex-row select-none text-sm"> | ||
<Menu side="left"> | ||
<Menu.Item | ||
name="Configuration" | ||
icon={<SlidersIcon size={leftIconSize} />} | ||
group={0} | ||
/> | ||
<Menu.Item | ||
name="Storage" | ||
icon={<DatabaseIcon size={leftIconSize} />} | ||
group={0} | ||
/> | ||
<Menu.Item | ||
name="Activity" | ||
icon={<ActivityIcon size={leftIconSize} />} | ||
group={1} | ||
/> | ||
<Menu.Item | ||
name="Settings" | ||
icon={<SettingsIcon size={leftIconSize} />} | ||
group={1} | ||
/> | ||
</Menu> | ||
Comment on lines
+27
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Consider extracting menu items configuration. The menu items are currently hardcoded in the component. Consider extracting them into a configuration object for better maintainability and reusability. +// In a new file: source/titfront/src/config/menuConfig.ts
+import { IconType } from 'react-icons';
+import { SlidersIcon, DatabaseIcon, ActivityIcon, SettingsIcon } from "react-icons/fi";
+
+interface MenuItem {
+ name: string;
+ icon: IconType;
+ group: number;
+}
+
+export const LEFT_MENU_ITEMS: MenuItem[] = [
+ { name: "Configuration", icon: SlidersIcon, group: 0 },
+ { name: "Storage", icon: DatabaseIcon, group: 0 },
+ { name: "Activity", icon: ActivityIcon, group: 1 },
+ { name: "Settings", icon: SettingsIcon, group: 1 },
+]; Also applies to: 51-65 |
||
<div className="flex-1 flex flex-col"> | ||
<Viewer /> | ||
<Menu side="bottom"> | ||
<Menu.Item | ||
name="Python shell" | ||
icon={<PythonIcon size={bottomIconSize} />} | ||
group={0} | ||
> | ||
<PythonShell /> | ||
</Menu.Item> | ||
<Menu.Item | ||
name="Console" | ||
icon={<TerminalIcon size={bottomIconSize} />} | ||
group={0} | ||
/> | ||
</Menu> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,191 @@ | ||||||||||||||||||||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\ | ||||||||||||||||||||||
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause. | ||||||||||||||||||||||
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md | ||||||||||||||||||||||
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ | ||||||||||||||||||||||
|
||||||||||||||||||||||
import { | ||||||||||||||||||||||
FC, | ||||||||||||||||||||||
Fragment, | ||||||||||||||||||||||
ReactNode, | ||||||||||||||||||||||
ReactElement, | ||||||||||||||||||||||
createContext, | ||||||||||||||||||||||
useContext, | ||||||||||||||||||||||
useMemo, | ||||||||||||||||||||||
} from "react"; | ||||||||||||||||||||||
import { IconBaseProps } from "react-icons"; | ||||||||||||||||||||||
import { FiMinimize as MinimizeIcon } from "react-icons/fi"; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import { Resizable } from "~/components/Resizable"; | ||||||||||||||||||||||
import { useMenuStore } from "~/stores/LayoutStore"; | ||||||||||||||||||||||
import { Side, iota, cn } from "~/utils"; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||||||||||||||||||
|
||||||||||||||||||||||
interface Action { | ||||||||||||||||||||||
key: string; | ||||||||||||||||||||||
icon: ReactElement<IconBaseProps>; | ||||||||||||||||||||||
callback: () => void; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
export interface MenuProviderProps { | ||||||||||||||||||||||
actions: Action[]; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
const MenuContext = createContext<MenuProviderProps | null>(null); | ||||||||||||||||||||||
|
||||||||||||||||||||||
export function useMenu() { | ||||||||||||||||||||||
return useContext(MenuContext)!; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
Comment on lines
+41
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove non-null assertion and add error handling. The export function useMenu() {
- return useContext(MenuContext)!;
+ const context = useContext(MenuContext);
+ if (context === null) {
+ throw new Error("useMenu must be used within a MenuContext.Provider");
+ }
+ return context;
} 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (1.9.4)[error] 37-37: Forbidden non-null assertion. (lint/style/noNonNullAssertion) 🪛 GitHub Check: codecov/patch[warning] 36-37: source/titfront/src/components/Menu.tsx#L36-L37 |
||||||||||||||||||||||
|
||||||||||||||||||||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||||||||||||||||||
|
||||||||||||||||||||||
interface MenuProps { | ||||||||||||||||||||||
side: Side; | ||||||||||||||||||||||
children: ReactElement<MenuItemProps>[]; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
export const Menu: FC<MenuProps> & { Item: FC<MenuItemProps> } = ({ | ||||||||||||||||||||||
side, | ||||||||||||||||||||||
children = [], | ||||||||||||||||||||||
}: MenuProps) => { | ||||||||||||||||||||||
const { size, setSize, activeItem, setActiveItem } = useMenuStore(side); | ||||||||||||||||||||||
|
||||||||||||||||||||||
/** @todo We should have a way to add custom actions from the children. */ | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Address the TODO comment about custom actions. The comment indicates a need for custom actions from children. Consider implementing this feature or creating an issue to track it. Would you like me to help implement custom actions support or create an issue to track this task? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, please, make a sketch on how this can be done. I want the actions to be added from withing the child of a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Jhuighuy Here's a sketch of how we can implement custom actions from children: // Modified context interface
interface MenuContextProps {
actions: Action[];
registerAction: (action: Action) => void;
unregisterAction: (key: string) => void;
}
// Updated Menu component
export const Menu: FC<MenuProps> & { Item: FC<MenuItemProps> } = ({
side,
children = [],
}: MenuProps) => {
const { size, setSize, activeItem, setActiveItem } = useMenuStore(side);
const [customActions, setCustomActions] = useState<Action[]>([]);
const registerAction = useCallback((action: Action) => {
setCustomActions(prev => [...prev, action]);
}, []);
const unregisterAction = useCallback((key: string) => {
setCustomActions(prev => prev.filter(action => action.key !== key));
}, []);
const contextValue = useMemo(() => ({
actions: [
// Default minimize action
{
key: "minimize",
icon: <MinimizeIcon />,
callback: () => setActiveItem(-1),
},
...customActions,
],
registerAction,
unregisterAction,
}), [customActions, setActiveItem]);
return (
<MenuContext.Provider value={contextValue}>
{/* ... rest of the component ... */}
</MenuContext.Provider>
);
};
// Example usage in a child component
const MyCustomMenuItem = () => {
const { registerAction, unregisterAction } = useMenu();
useEffect(() => {
const action = {
key: "custom-action",
icon: <CustomIcon />,
callback: () => {
// Custom action logic
},
};
registerAction(action);
return () => unregisterAction(action.key);
}, []);
return <div>Custom Menu Item</div>;
};
// Usage
<Menu side="left">
<Menu.Item>
<MyCustomMenuItem />
</Menu.Item>
</Menu> This approach:
Would you like me to explain any part in more detail? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we encapsulate the logic of registering and unregistering into a custom hook? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Jhuighuy Here's how we can encapsulate the logic into a custom hook: // Custom hook for action management
const useMenuAction = (action: Action) => {
const { registerAction, unregisterAction } = useMenu();
useEffect(() => {
registerAction(action);
return () => unregisterAction(action.key);
}, [action]);
};
// Example usage in a child component
const MyCustomMenuItem = () => {
useMenuAction({
key: "custom-action",
icon: <CustomIcon />,
callback: () => {
// Custom action logic
},
});
return <div>Custom Menu Item</div>;
}; This simplifies the usage and ensures consistent cleanup. Would you like me to show how to handle dynamic action updates as well? |
||||||||||||||||||||||
const actions = useMemo<MenuProviderProps>( | ||||||||||||||||||||||
() => ({ | ||||||||||||||||||||||
actions: [ | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
key: "hide", | ||||||||||||||||||||||
icon: <MinimizeIcon size={16} />, | ||||||||||||||||||||||
callback: () => setActiveItem(-1), | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
], | ||||||||||||||||||||||
}), | ||||||||||||||||||||||
[setActiveItem] | ||||||||||||||||||||||
); | ||||||||||||||||||||||
|
||||||||||||||||||||||
const maxGroup = children.reduce( | ||||||||||||||||||||||
(max, child) => Math.max(max, child.props.group), | ||||||||||||||||||||||
0 | ||||||||||||||||||||||
); | ||||||||||||||||||||||
|
||||||||||||||||||||||
const horizontal = side === "left" || side === "right"; | ||||||||||||||||||||||
const menuCn = cn( | ||||||||||||||||||||||
"flex text-gray-300", | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
left: "flex-row", | ||||||||||||||||||||||
right: "flex-row-reverse", | ||||||||||||||||||||||
top: "flex-col", | ||||||||||||||||||||||
bottom: "flex-col-reverse", | ||||||||||||||||||||||
}[side] | ||||||||||||||||||||||
); | ||||||||||||||||||||||
const menuBarCn = cn( | ||||||||||||||||||||||
"flex items-center justify-between", | ||||||||||||||||||||||
"bg-gradient-to-bl from-gray-800 to-gray-950", | ||||||||||||||||||||||
horizontal ? "flex-col w-16 h-full gap-6" : "w-full h-8 pl-1" | ||||||||||||||||||||||
); | ||||||||||||||||||||||
const baseItemCn = cn( | ||||||||||||||||||||||
"flex items-center", | ||||||||||||||||||||||
horizontal | ||||||||||||||||||||||
? "justify-center size-11 my-6 rounded-lg" | ||||||||||||||||||||||
: "w-30 h-6 pl-1 rounded-sm" | ||||||||||||||||||||||
); | ||||||||||||||||||||||
const itemCn = cn( | ||||||||||||||||||||||
baseItemCn, | ||||||||||||||||||||||
"hover:bg-gradient-to-bl hover:from-gray-700 hover:to-gray-800", | ||||||||||||||||||||||
"hover:text-gray-100 hover:shadow-xl" | ||||||||||||||||||||||
); | ||||||||||||||||||||||
const activeItemCn = cn( | ||||||||||||||||||||||
baseItemCn, | ||||||||||||||||||||||
"inset-shadow-sm inset-shadow-gray-700", | ||||||||||||||||||||||
"bg-gradient-to-bl from-gray-700 to-indigo-900 text-gray-100" | ||||||||||||||||||||||
); | ||||||||||||||||||||||
return ( | ||||||||||||||||||||||
<div className={menuCn}> | ||||||||||||||||||||||
{/* Menu bar. */} | ||||||||||||||||||||||
<div className={menuBarCn}> | ||||||||||||||||||||||
{iota(maxGroup + 1).map((group) => ( | ||||||||||||||||||||||
<div key={group} className={cn(horizontal || "flex items-center")}> | ||||||||||||||||||||||
{children.map( | ||||||||||||||||||||||
(item, index) => | ||||||||||||||||||||||
item.props.group == group && ( | ||||||||||||||||||||||
<Fragment key={index}> | ||||||||||||||||||||||
<button | ||||||||||||||||||||||
className={index === activeItem ? activeItemCn : itemCn} | ||||||||||||||||||||||
onClick={() => setActiveItem(index)} | ||||||||||||||||||||||
> | ||||||||||||||||||||||
{item.props.icon} | ||||||||||||||||||||||
{horizontal || ( | ||||||||||||||||||||||
<span className="ml-1">{item.props.name}</span> | ||||||||||||||||||||||
)} | ||||||||||||||||||||||
</button> | ||||||||||||||||||||||
Comment on lines
+135
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add type attributes to button elements. The button elements should have explicit type attributes to prevent form submission behavior. <button
className={index === activeItem ? activeItemCn : itemCn}
onClick={() => setActiveItem(index)}
+ type="button"
>
<button
key={key}
className={cn(
"w-6 h-6 mr-2 rounded flex items-center justify-center",
"hover:bg-gray-500"
)}
onClick={callback}
+ type="button"
> Also applies to: 166-176 🧰 Tools🪛 Biome (1.9.4)[error] 113-116: Provide an explicit type prop for the button element. The default type of a button is submit, which causes the submission of a form when placed inside a (lint/a11y/useButtonType) 🪛 GitHub Check: codecov/patch[warning] 117-119: source/titfront/src/components/Menu.tsx#L117-L119 |
||||||||||||||||||||||
{horizontal || ( | ||||||||||||||||||||||
<div className="w-[1px] h-6 mx-1 bg-gray-600" /> | ||||||||||||||||||||||
)} | ||||||||||||||||||||||
</Fragment> | ||||||||||||||||||||||
) | ||||||||||||||||||||||
)} | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
))} | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
{/* Active item. */} | ||||||||||||||||||||||
{activeItem !== -1 && ( | ||||||||||||||||||||||
<Resizable side={side} size={size} setSize={setSize}> | ||||||||||||||||||||||
<MenuContext.Provider value={actions}> | ||||||||||||||||||||||
{children[activeItem]} | ||||||||||||||||||||||
</MenuContext.Provider> | ||||||||||||||||||||||
</Resizable> | ||||||||||||||||||||||
)} | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
); | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||||||||||||||||||
|
||||||||||||||||||||||
interface MenuItemProps { | ||||||||||||||||||||||
name: string; | ||||||||||||||||||||||
icon: ReactElement<IconBaseProps>; | ||||||||||||||||||||||
group: number; | ||||||||||||||||||||||
children?: ReactNode; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
const MenuItem: FC<MenuItemProps> = ({ name, children }) => { | ||||||||||||||||||||||
const { actions } = useMenu(); | ||||||||||||||||||||||
return ( | ||||||||||||||||||||||
<div | ||||||||||||||||||||||
className={cn( | ||||||||||||||||||||||
"size-full flex flex-col", | ||||||||||||||||||||||
"bg-gradient-to-bl from-gray-700 to-gray-800" | ||||||||||||||||||||||
)} | ||||||||||||||||||||||
> | ||||||||||||||||||||||
{/* Title. */} | ||||||||||||||||||||||
<div className="h-4 m-1 mt-2 flex flex-row items-center justify-between"> | ||||||||||||||||||||||
<span className="ml-2 font-medium truncate">{name.toUpperCase()}</span> | ||||||||||||||||||||||
<div className="flex flex-row items-center"> | ||||||||||||||||||||||
{actions.map(({ key, icon, callback }) => ( | ||||||||||||||||||||||
<button | ||||||||||||||||||||||
key={key} | ||||||||||||||||||||||
className={cn( | ||||||||||||||||||||||
"w-6 h-6 mr-2 rounded flex items-center justify-center", | ||||||||||||||||||||||
"hover:bg-gray-500" | ||||||||||||||||||||||
)} | ||||||||||||||||||||||
onClick={callback} | ||||||||||||||||||||||
> | ||||||||||||||||||||||
{icon} | ||||||||||||||||||||||
</button> | ||||||||||||||||||||||
))} | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
{/* Contents. */} | ||||||||||||||||||||||
<div | ||||||||||||||||||||||
className={cn("flex-grow m-1 rounded-lg overflow-auto", "bg-gray-900")} | ||||||||||||||||||||||
> | ||||||||||||||||||||||
{children} | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
</div> | ||||||||||||||||||||||
); | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
Menu.Item = MenuItem; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *\ | ||
* Part of BlueTit Solver, licensed under Apache 2.0 with Commons Clause. | ||
* Commercial use, including SaaS, requires a separate license, see /LICENSE.md | ||
\* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ | ||
|
||
import "@testing-library/jest-dom/vitest"; | ||
import { render, screen, fireEvent } from "@testing-library/react"; | ||
import { describe, it, expect } from "vitest"; | ||
import { vi } from "vitest"; | ||
|
||
import { Resizable } from "~/components/Resizable"; | ||
|
||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
describe("Resizable", () => { | ||
it("renders children correctly", () => { | ||
const { getByText } = render( | ||
<Resizable side="left" size={200} setSize={() => {}}> | ||
<div>Test</div> | ||
</Resizable> | ||
); | ||
expect(getByText("Test")).toBeInTheDocument(); | ||
}); | ||
|
||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
it("applies initial width correctly", () => { | ||
render( | ||
<Resizable side="left" size={200} setSize={() => {}}> | ||
<div>Test</div> | ||
</Resizable> | ||
); | ||
expect(screen.getByTestId("resizable-content")).toHaveStyle("width: 200px"); | ||
}); | ||
|
||
it("applies initial height correctly", () => { | ||
render( | ||
<Resizable side="bottom" size={200} setSize={() => {}}> | ||
<div>Test</div> | ||
</Resizable> | ||
); | ||
expect(screen.getByTestId("resizable-content")).toHaveStyle( | ||
"height: 200px" | ||
); | ||
}); | ||
|
||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
it("handles width changes with mouse correctly", () => { | ||
const setSize = vi.fn(); | ||
render( | ||
<Resizable side="left" size={200} setSize={setSize}> | ||
<div>Test Content</div> | ||
</Resizable> | ||
); | ||
|
||
const resizer = screen.getByTestId("resizable-resizer"); | ||
|
||
fireEvent.mouseDown(resizer, { clientX: 0 }); | ||
fireEvent.mouseMove(window, { clientX: 50 }); | ||
expect(document.body.style.cursor).toBe("ew-resize"); | ||
expect(setSize).toHaveBeenCalledWith(250); | ||
|
||
fireEvent.mouseUp(window); | ||
expect(document.body.style.cursor).toBe(""); | ||
}); | ||
|
||
it("handles height changes with mouse correctly", () => { | ||
const setSize = vi.fn(); | ||
render( | ||
<Resizable side="bottom" size={200} setSize={setSize}> | ||
<div>Test Content</div> | ||
</Resizable> | ||
); | ||
const resizer = screen.getByTestId("resizable-resizer"); | ||
|
||
fireEvent.mouseDown(resizer, { clientY: 0 }); | ||
fireEvent.mouseMove(window, { clientY: -50 }); | ||
expect(document.body.style.cursor).toBe("ns-resize"); | ||
expect(setSize).toHaveBeenCalledWith(250); | ||
|
||
fireEvent.mouseUp(window); | ||
expect(document.body.style.cursor).toBe(""); | ||
}); | ||
|
||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
it("respects size constraints", () => { | ||
const setSize = vi.fn(); | ||
render( | ||
<Resizable | ||
side="left" | ||
size={200} | ||
setSize={setSize} | ||
minSize={100} | ||
maxSize={300} | ||
> | ||
<div>Test Content</div> | ||
</Resizable> | ||
); | ||
const resizer = screen.getByTestId("resizable-resizer"); | ||
|
||
fireEvent.mouseDown(resizer, { clientX: 0 }); | ||
fireEvent.mouseMove(window, { clientX: 50 }); | ||
expect(setSize).toHaveBeenCalledWith(250); | ||
|
||
fireEvent.mouseMove(window, { clientX: 150 }); | ||
expect(setSize).toHaveBeenCalledWith(300); | ||
|
||
fireEvent.mouseUp(window); | ||
|
||
fireEvent.mouseDown(resizer, { clientX: 0 }); | ||
fireEvent.mouseMove(window, { clientX: -50 }); | ||
expect(setSize).toHaveBeenCalledWith(150); | ||
|
||
fireEvent.mouseMove(window, { clientX: -150 }); | ||
expect(setSize).toHaveBeenCalledWith(100); | ||
|
||
fireEvent.mouseUp(window); | ||
}); | ||
}); | ||
|
||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Consider extracting icon sizes into constants or theme configuration.
The hardcoded icon sizes (
leftIconSize = 24
andbottomIconSize = 16
) should be moved to a theme configuration to maintain consistency across the application.Also applies to: 25-25