diff --git a/packages/ui/responsive/README.md b/packages/ui/responsive/README.md new file mode 100644 index 0000000..1846287 --- /dev/null +++ b/packages/ui/responsive/README.md @@ -0,0 +1,48 @@ +
+ @ibrahimstudio/responsive +
+

@ibrahimstudio/responsive

+

by: Ibrahim Space Studio

+

This package provides automatic responsiveness for React applications.

+
+
+ +--- + +## 1. Installation + +You can install this package via npm: + +```sh +npm i @ibrahimstudio/responsive +# or +yarn add @ibrahimstudio/responsive +``` + +## 2. Usage + +To use `@ibrahimstudio/responsive`, simply wrap your entire React application with the `` component: + +```javascript +import React from "react"; +import ReactDOM from "react-dom/client"; +import { ISResponsive } from "@ibrahimstudio/responsive"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + + + +); +``` + +This will enable automatic responsiveness for your entire application by detecting elements with `px` in every elements. It calculates and adjusts these values based on the device type, ensuring optimal display across different screen sizes. + +## 3. Contributing + +Contributions are welcome! If you have any improvements, bug fixes, or features, feel free to open an issue or create a pull request on GitHub. + +## 4. License + +This project is licensed under the MIT License - see the [LICENSE](https://github.com/space-ibrahimstudio/ibrahimstudio/blob/master/LICENSE) file for details. diff --git a/packages/ui/responsive/__tests__/responsive.test.tsx b/packages/ui/responsive/__tests__/responsive.test.tsx new file mode 100644 index 0000000..1bc54df --- /dev/null +++ b/packages/ui/responsive/__tests__/responsive.test.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { useDeviceType, usePixelConverter } from "@ibrahimstudio/hooks"; +import ISResponsive from "../src/responsive"; +import "@testing-library/jest-dom"; + +jest.mock("react", () => ({ + ...jest.requireActual("react"), + useState: jest.fn(), + useEffect: jest.fn(), +})); + +describe("useDeviceType", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return "desktop" when window width is greater than 1120px', () => { + (window as any).innerWidth = 1200; + (React.useState as jest.Mock).mockReturnValueOnce(["desktop", jest.fn()]); + + const deviceType = useDeviceType(); + expect(deviceType).toBe("desktop"); + }); + + it('should return "tablet" when window width is less than or equal to 1120px', () => { + (window as any).innerWidth = 800; + (React.useState as jest.Mock).mockReturnValueOnce(["tablet", jest.fn()]); + + const deviceType = useDeviceType(); + expect(deviceType).toBe("tablet"); + }); + + it('should return "mobile" when window width is less than or equal to 700px', () => { + (window as any).innerWidth = 500; + (React.useState as jest.Mock).mockReturnValueOnce(["mobile", jest.fn()]); + + const deviceType = useDeviceType(); + expect(deviceType).toBe("mobile"); + }); +}); + +describe("usePixelConverter", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should convert pixel value based on device type", () => { + expect(usePixelConverter("100px", "mobile")).toBe("80px"); + expect(usePixelConverter("100px", "tablet")).toBe("90px"); + expect(usePixelConverter("100px", "desktop")).toBe("100px"); + }); + + it("should return the original value if not in pixel format", () => { + expect(usePixelConverter("100%", "mobile")).toBe("100%"); + }); +}); + +describe("ISResponsive", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should render children with desktop device", () => { + (window as any).innerWidth = 1200; + (React.useState as jest.Mock).mockReturnValueOnce(["desktop", jest.fn()]); + + const { getByTestId } = render( + +
+ + ); + + const testElement = getByTestId("test"); + expect(testElement).toHaveStyle("width: 100px"); + expect(testElement).toHaveStyle("height: 100px"); + }); + + it("should render children with tablet device", () => { + (window as any).innerWidth = 800; + (React.useState as jest.Mock).mockReturnValueOnce(["tablet", jest.fn()]); + + const { getByTestId } = render( + +
+ + ); + + const testElement = getByTestId("test"); + expect(testElement).toHaveStyle("width: 90px"); + expect(testElement).toHaveStyle("height: 90px"); + }); + + it("should render children with mobile device", () => { + (window as any).innerWidth = 500; + (React.useState as jest.Mock).mockReturnValueOnce(["mobile", jest.fn()]); + + const { getByTestId } = render( + +
+ + ); + + const testElement = getByTestId("test"); + expect(testElement).toHaveStyle("width: 80px"); + expect(testElement).toHaveStyle("height: 80px"); + }); +}); diff --git a/packages/ui/responsive/src/index.ts b/packages/ui/responsive/src/index.ts new file mode 100644 index 0000000..f435112 --- /dev/null +++ b/packages/ui/responsive/src/index.ts @@ -0,0 +1 @@ +export { default as ISResponsive } from "./responsive"; diff --git a/packages/ui/responsive/src/responsive.tsx b/packages/ui/responsive/src/responsive.tsx new file mode 100644 index 0000000..669a7d8 --- /dev/null +++ b/packages/ui/responsive/src/responsive.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { useDeviceType, usePixelConverter } from "@ibrahimstudio/hooks"; + +type DeviceType = "mobile" | "tablet" | "desktop"; + +const ISResponsive: React.FC> = ({ children }) => { + const [deviceType, setDeviceType] = React.useState("desktop"); + + React.useEffect(() => { + const handleResize = () => { + const newDeviceType = useDeviceType(); + if (newDeviceType !== deviceType) { + setDeviceType(newDeviceType); + } + }; + + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [deviceType]); + + const processStyles = (styles: React.CSSProperties): React.CSSProperties => { + const processedStyles: React.CSSProperties = {}; + for (const key in styles) { + if (styles.hasOwnProperty(key)) { + const propKey = key as keyof React.CSSProperties; + const styleValue = styles[propKey]; + if (typeof styleValue === "string" && styleValue.includes("px")) { + (processedStyles as any)[propKey] = usePixelConverter( + styleValue, + deviceType + ); + } else { + (processedStyles as any)[propKey] = styleValue; + } + } + } + return processedStyles; + }; + + const executeChildren = (children: React.ReactNode): React.ReactNode => { + return React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + const updatedProps = { + ...child.props, + style: processStyles(child.props.style || {}), + }; + return React.cloneElement( + child, + updatedProps, + executeChildren(child.props.children) + ); + } + return child; + }); + }; + + return <>{executeChildren(children)}; +}; + +export default ISResponsive;