diff --git a/README.md b/README.md index f6cbdad..9f9c4c8 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ export function Komponent() { } ``` +> Note: The element resizing is automatically debounced with a delay of 200ms. You can override this delay by setting the `debounce` prop with a number in milliseconds. + Also the gap between columns can be set by setting the `gap` prop: ```jsx diff --git a/src/components/Plock.js b/src/components/Plock.js index e0e4982..6190902 100644 --- a/src/components/Plock.js +++ b/src/components/Plock.js @@ -1,7 +1,5 @@ import * as React from "react"; -const uuid = () => Math.random().toString(36).substring(2, 12); - /** * Configuration for Plock. * This is a map of breakpoints to the number of columns to use for that breakpoint. @@ -14,84 +12,126 @@ const uuid = () => Math.random().toString(36).substring(2, 12); * ]; */ -export function useWindowWidth() { +export function useWindowWidth({ debounceMs }) { const [width, setWidth] = React.useState(window.innerWidth); + const handleResize = useDebounce( + () => setWidth(window.innerWidth), + debounceMs + ); React.useEffect(() => { - const handleResize = () => setWidth(window.innerWidth); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); - }, []); + }, [handleResize]); return width; } -export function Plock({ children, className, style, nColumns = 3, gap = 10 }) { - const width = useWindowWidth(); - const [columns, setColumns] = React.useState([]); - - React.useLayoutEffect(() => { - let columnsElements = []; - - if (typeof nColumns === "number") { - columnsElements = Array.from({ length: nColumns }, (e) => []); - } else { - let breakpoint = nColumns - .filter((el) => el.size <= width) - .sort((a, b) => a.size - b.size) - .pop(); - - if (!breakpoint) { - breakpoint = nColumns.sort((a, b) => a.size - b.size)[0]; - } - - columnsElements = Array.from({ length: breakpoint.columns }, (e) => []); - } - - React.Children.forEach(children, (child, index) => { - const key = uuid(); - const cloned = React.cloneElement(child, { - ...child.props, - key: key, - }); +export function useDebounce(fn, ms) { + let timeout = null; - columnsElements[index % columnsElements.length].push(cloned); - }); + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => fn(args), ms); + }; +} - setColumns(columnsElements); - }, [children, nColumns, setColumns, width]); +export const Plock = React.forwardRef( + ( + { + as: Comp = "div", + children, + className, + style, + gap = 10, + debounce = 200, - const defaultStyles = { - mainGrid: { - display: "grid", - gridTemplateColumns: `repeat(${columns.length}, 1fr)`, - columnGap: gap, - alignItems: "start", - }, - columnGrid: { - display: "grid", - gridTemplateColumns: "100%", - rowGap: gap, + /** + * TODO: + * This will be renamed to breakpoints in a future major release! + */ + nColumns: breakpoints = 3, }, - }; + forwardedRef + ) => { + const width = useWindowWidth({ debounceMs: debounce }); + const [columns, setColumns] = React.useState([]); - return ( -
- {columns.map((column, index) => { - return ( -
- {column} -
- ); - })} -
- ); -} + React.useLayoutEffect(() => { + const first = (breakpoints) => { + return breakpoints?.[0]; + }; + + const last = (breakpoints) => { + return breakpoints?.[breakpoints.length - 1]; + }; + + const sorted = (breakpoints) => { + return breakpoints.sort((a, b) => a.size - b.size); + }; + + const contained = (breakpoints, width) => { + return breakpoints.filter((el) => el.size <= width); + }; + + const isNumber = (element) => typeof element === "number"; + + const breakpoint = isNumber(breakpoints) + ? { columns: breakpoints } + : last(sorted(contained(breakpoints, width))) ?? + first(sorted(breakpoints)); + + const columnsForBreakpoint = Array.from( + { length: breakpoint.columns }, + (e) => [] + ); + + React.Children.forEach(children, (child, index) => { + const key = `item-${index}`; + const cloned = React.cloneElement(child, { + ...child.props, + key: key, + }); + + columnsForBreakpoint[index % columnsForBreakpoint.length].push(cloned); + }); + + setColumns(columnsForBreakpoint); + }, [children, breakpoints, width]); + + const defaultStyles = { + mainGrid: { + display: "grid", + gridTemplateColumns: `repeat(${columns.length}, 1fr)`, + columnGap: gap, + alignItems: "start", + }, + columnGrid: { + display: "grid", + gridTemplateColumns: "100%", + rowGap: gap, + }, + }; + + return ( + + {columns.map((column, index) => { + return ( +
+ {column} +
+ ); + })} +
+ ); + } +); diff --git a/src/components/Plock.test.js b/src/components/Plock.test.js index da2ab53..1b4ee2d 100644 --- a/src/components/Plock.test.js +++ b/src/components/Plock.test.js @@ -1,5 +1,5 @@ import matchMediaPolyfill from "mq-polyfill"; -import { render, screen } from "@testing-library/react"; +import { render, screen, act } from "@testing-library/react"; import { Plock } from "./Plock"; beforeAll(() => { @@ -148,7 +148,9 @@ it("should render one column with a 500px window", () => { { size: 1280, columns: 6 }, ]; - window.resizeTo(500, 1000); + act(() => { + window.resizeTo(500, 1000); + }); render( @@ -168,7 +170,9 @@ it("should render one column with a 639px window", () => { { size: 1280, columns: 6 }, ]; - window.resizeTo(639, 1000); + act(() => { + window.resizeTo(639, 1000); + }); render( @@ -188,7 +192,9 @@ it("should render two columns with a 768px window", () => { { size: 1280, columns: 6 }, ]; - window.resizeTo(768, 1000); + act(() => { + window.resizeTo(768, 1000); + }); render( @@ -208,7 +214,9 @@ it("should render two columns with a 800px window", () => { { size: 1280, columns: 6 }, ]; - window.resizeTo(800, 1000); + act(() => { + window.resizeTo(800, 1000); + }); render( @@ -228,7 +236,9 @@ it("should render two columns with a 1023px window", () => { { size: 1280, columns: 6 }, ]; - window.resizeTo(1023, 1000); + act(() => { + window.resizeTo(1023, 1000); + }); render( @@ -248,7 +258,9 @@ it("should render three columns with a 1024px window", () => { { size: 1280, columns: 6 }, ]; - window.resizeTo(1024, 1000); + act(() => { + window.resizeTo(1024, 1000); + }); render( @@ -268,7 +280,9 @@ it("should render six columns with a 1280px window", () => { { size: 1280, columns: 6 }, ]; - window.resizeTo(1280, 1000); + act(() => { + window.resizeTo(1280, 1000); + }); render( @@ -287,15 +301,47 @@ it("should keep the default number of columns if a number is passed", () => { ); - window.resizeTo(100, 1000); + act(() => { + window.resizeTo(100, 1000); + }); expect(screen.getAllByTestId("plock-column")).toHaveLength(3); - window.resizeTo(500, 1000); + act(() => { + window.resizeTo(500, 1000); + }); expect(screen.getAllByTestId("plock-column")).toHaveLength(3); - window.resizeTo(1000, 1000); + act(() => { + window.resizeTo(1000, 1000); + }); expect(screen.getAllByTestId("plock-column")).toHaveLength(3); - window.resizeTo(2000, 1000); + act(() => { + window.resizeTo(2000, 1000); + }); expect(screen.getAllByTestId("plock-column")).toHaveLength(3); }); + +it("should be render a div as a container by default", () => { + render(); + + const element = screen.getByTestId("plock-container"); + expect(element).toBeInTheDocument(); + expect(element.tagName).toEqual("DIV"); +}); + +it("should be possible to override the container", () => { + render(); + + const element = screen.getByTestId("plock-container"); + expect(element).toBeInTheDocument(); + expect(element.tagName).toEqual("SECTION"); +}); + +it("should have not the component class if i override it", () => { + const Component = (props) =>
; + render(); + + const element = screen.getByTestId("plock-container"); + expect(element).not.toHaveClass("sku"); +}); diff --git a/src/index.js b/src/index.js index 8d6d2b6..0f3ceb0 100644 --- a/src/index.js +++ b/src/index.js @@ -8,10 +8,11 @@ function App() { { size: 768, columns: 2 }, { size: 1024, columns: 3 }, { size: 1280, columns: 6 }, + { size: 1680, columns: 8 }, ]; return ( - +
1
2
3