From 81c4b26733d712119b0a71c6fcdd05dba320fea4 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 15 Sep 2022 11:09:33 +0200 Subject: [PATCH] feat(use-positioner): add maxColumnCount property --- README.md | 45 ++++---- src/index.test.tsx | 35 +++++++ src/masonry.tsx | 6 +- src/use-positioner.ts | 29 +++++- types/use-positioner.d.ts | 214 +++++++++++++++++++++----------------- 5 files changed, 208 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index e33c636..ebdddc7 100755 --- a/README.md +++ b/README.md @@ -167,12 +167,13 @@ const MasonryCard = ({ index, data: { id }, width }) => ( Props for tuning the column width, count, and gutter of your component. -| Prop | Type | Default | Required? | Description | -| ------------ | -------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| columnWidth | `number` | `240` | No | This is the minimum column width. `Masonic` will automatically size your columns to fill its container based on your provided `columnWidth` and `columnGutter` values. It will never render anything smaller than this defined width unless its container is smaller than its value. | -| columnGutter | `number` | `0` | No | This sets the horizontal space between grid columns in pixels. If `rowGutter` is not set, this also sets the vertical space between cells within a column in pixels. | -| rowGutter | `number` | Same as `columnGutter` | No | This sets the vertical space between cells within a column in pixels. | -| columnCount | `number` | | No | By default, `Masonic` derives the column count from the `columnWidth` prop. However, in some situations it is nice to be able to override that behavior e.g. when creating a [``](#list). | +| Prop | Type | Default | Required? | Description | +| -------------- | -------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| columnWidth | `number` | `240` | No | This is the minimum column width. `Masonic` will automatically size your columns to fill its container based on your provided `columnWidth` and `columnGutter` values. It will never render anything smaller than this defined width unless its container is smaller than its value. | +| columnGutter | `number` | `0` | No | This sets the horizontal space between grid columns in pixels. If `rowGutter` is not set, this also sets the vertical space between cells within a column in pixels. | +| rowGutter | `number` | Same as `columnGutter` | No | This sets the vertical space between cells within a column in pixels. | +| columnCount | `number` | | No | By default, `Masonic` derives the column count from the `columnWidth` prop. However, in some situations it is nice to be able to override that behavior e.g. when creating a [``](#list). | +| maxColumnCount | `number` | | No | Limits the number of columns used by `Masonic`. Useful for implementing responsive layouts. | **Grid container props** @@ -273,7 +274,7 @@ const MyMasonry = (props) => { #### Props In addition to these props, this component accepts all of the props outlined in [``](#masonry) -with exception to `columnGutter`, `rowGutter`, `columnWidth`, `columnCount`, `ssrWidth`, and `ssrHeight`. +with exception to `columnGutter`, `rowGutter`, `columnWidth`, `columnCount`, `maxColumntCount`, `ssrWidth`, and `ssrHeight`. | Prop | Type | Default | Required? | Description | | -------------- | ------------------------------------------------------------------- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -315,7 +316,7 @@ const ListCard = ({ index, data: { id }, width }) => ( #### Props In addition to these props, this component accepts all of the props outlined in [``](#masonry) -with exception to `columnGutter`, `columnWidth`, and `columnCount`. +with exception to `columnGutter`, `columnWidth`, `columnCount`, and `maxColumnCount`. | Prop | Type | Default | Required? | Description | | --------- | -------- | ------- | --------- | ---------------------------------------------------------------------- | @@ -442,13 +443,14 @@ const MyMasonry = ({ columnWidth = 300, columnGutter = 16, ...props }) => { #### UsePositionerOptions -| Argument | Type | Default | Required? | Description | -| ------------ | -------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| width | `number` | | Yes | The width of the container you're rendering the grid within, e.g. the container element's `element.offsetWidth`. That said, you can provide any width here. | -| columnWidth | `number` | `200` | No | The minimum column width. The [`usePositioner()`](#usepositioneroptions-deps) hook will automatically size the columns to fill their container based upon the `columnWidth` and `columnGutter` values. It will never render anything smaller than this width unless its container itself is smaller than its value. This property has no effect if you're providing a `columnCount`. | -| columnGutter | `number` | `0` | No | This sets the horizontal space between grid columns in pixels. If `rowGutter` is not set, this also sets the vertical space between cells within a column in pixels. | -| rowGutter | `number` | Same as `columnGutter` | No | This sets the vertical space between cells within a column in pixels. | -| columnCount | `number` | | No | By default, [`usePositioner()`](#usepositioneroptions-deps) derives the column count from the `columnWidth`, `columnGutter`, and `width` props. However, in some situations it is nice to be able to override that behavior (e.g. creating a [``-like](#list) component). | +| Argument | Type | Default | Required? | Description | +| -------------- | -------- | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| width | `number` | | Yes | The width of the container you're rendering the grid within, e.g. the container element's `element.offsetWidth`. That said, you can provide any width here. | +| columnWidth | `number` | `200` | No | The minimum column width. The [`usePositioner()`](#usepositioneroptions-deps) hook will automatically size the columns to fill their container based upon the `columnWidth` and `columnGutter` values. It will never render anything smaller than this width unless its container itself is smaller than its value. This property has no effect if you're providing a `columnCount`. | +| columnGutter | `number` | `0` | No | This sets the horizontal space between grid columns in pixels. If `rowGutter` is not set, this also sets the vertical space between cells within a column in pixels. | +| rowGutter | `number` | Same as `columnGutter` | No | This sets the vertical space between cells within a column in pixels. | +| columnCount | `number` | | No | By default, [`usePositioner()`](#usepositioneroptions-deps) derives the column count from the `columnWidth`, `columnGutter`, and `width` props. However, in some situations it is nice to be able to override that behavior (e.g. creating a [``-like](#list) component). | +| maxColumnCount | `number` | | No | Limits the number of columns used by [`usePositioner()`](#usepositioneroptions-deps). Useful for implementing responsive layouts. | #### Returns a [`Positioner`](#positioner) @@ -782,12 +784,13 @@ this utility under the hood. #### Arguments -| Argument | Type | Description | -| ------------ | -------- | ---------------------------------------------------------------------------------------------------- | -| columnCount | `number` | The number of columns in the grid | -| columnWidth | `number` | The width of each column in the grid | -| columnGutter | `number` | The amount of horizontal space between columns in pixels. | -| rowGutter | `number` | The amount of vertical space between cells within a column in pixels (falls back to `columnGutter`). | +| Argument | Type | Description | +| -------------- | -------- | ---------------------------------------------------------------------------------------------------- | +| columnCount | `number` | The number of columns in the grid | +| columnWidth | `number` | The width of each column in the grid | +| columnGutter | `number` | The amount of horizontal space between columns in pixels. | +| rowGutter | `number` | The amount of vertical space between cells within a column in pixels (falls back to `columnGutter`). | +| maxColumnCount | `number` | The upper bound of column count. | #### Returns [`Positioner`](#positioner) diff --git a/src/index.test.tsx b/src/index.test.tsx index 1da1045..04a47dc 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -408,6 +408,41 @@ describe("usePositioner()", () => { expect(result.current.columnWidth).toBe(418); }); + it("should automatically derive column width when a maximum column count is defined", () => { + const { result, rerender } = renderHook((props) => usePositioner(props), { + initialProps: { + width: 1280, + columnCount: undefined, + columnWidth: 20, + columnGutter: 10, + maxColumnCount: 4, + }, + }); + + expect(result.current.columnCount).toBe(4); + expect(result.current.columnWidth).toBe(312); + + rerender({ + width: 1280, + columnCount: undefined, + columnWidth: 20, + columnGutter: 10, + maxColumnCount: 5, + }); + expect(result.current.columnCount).toBe(5); + expect(result.current.columnWidth).toBe(248); + + rerender({ + width: 1280, + columnCount: 1, + columnWidth: 20, + columnGutter: 10, + maxColumnCount: 5, + }); + expect(result.current.columnCount).toBe(1); + expect(result.current.columnWidth).toBe(1280); + }); + it("should create a new positioner when sizing deps change", () => { const { result, rerender } = renderHook((props) => usePositioner(props), { initialProps: { width: 1280, columnCount: 4, columnGutter: 10 }, diff --git a/src/masonry.tsx b/src/masonry.tsx index 86fd270..afc8f11 100644 --- a/src/masonry.tsx +++ b/src/masonry.tsx @@ -63,7 +63,11 @@ export interface MasonryProps >, Pick< UsePositionerOptions, - "columnWidth" | "columnGutter" | "rowGutter" | "columnCount" + | "columnWidth" + | "columnGutter" + | "rowGutter" + | "columnCount" + | "maxColumnCount" > { /** * Scrolls to a given index within the grid. The grid will re-scroll diff --git a/src/use-positioner.ts b/src/use-positioner.ts index d4433b7..4781c30 100644 --- a/src/use-positioner.ts +++ b/src/use-positioner.ts @@ -15,6 +15,7 @@ import { createIntervalTree } from "./interval-tree"; * @param options.columnGutter * @param options.rowGutter * @param options.columnCount + * @param options.maxColumnCount */ export function usePositioner( { @@ -23,6 +24,7 @@ export function usePositioner( columnGutter = 0, rowGutter, columnCount, + maxColumnCount, }: UsePositionerOptions, deps: React.DependencyList = emptyArr ): Positioner { @@ -31,7 +33,8 @@ export function usePositioner( width, columnWidth, columnGutter, - columnCount + columnCount, + maxColumnCount ); return createPositioner( computedColumnCount, @@ -45,7 +48,14 @@ export function usePositioner( positionerRef.current = initPositioner(); const prevDeps = React.useRef(deps); - const opts = [width, columnWidth, columnGutter, rowGutter, columnCount]; + const opts = [ + width, + columnWidth, + columnGutter, + rowGutter, + columnCount, + maxColumnCount, + ]; const prevOpts = React.useRef(opts); const optsChanged = !opts.every((item, i) => prevOpts.current[i] === item); @@ -113,6 +123,10 @@ export interface UsePositionerOptions { * (e.g. creating a `List` component). */ columnCount?: number; + /** + * The upper bound of column count. This property won't work if `columnCount` is set. + */ + maxColumnCount?: number; } /** @@ -334,9 +348,16 @@ const getColumns = ( width = 0, minimumWidth = 0, gutter = 8, - columnCount?: number + columnCount?: number, + maxColumnCount?: number ): [number, number] => { - columnCount = columnCount || Math.floor((width + gutter) / (minimumWidth + gutter)) || 1; + columnCount = + columnCount || + Math.min( + Math.floor((width + gutter) / (minimumWidth + gutter)), + maxColumnCount || Infinity + ) || + 1; const columnWidth = Math.floor( (width - gutter * (columnCount - 1)) / columnCount ); diff --git a/types/use-positioner.d.ts b/types/use-positioner.d.ts index dcbf96e..4ef7d2c 100644 --- a/types/use-positioner.d.ts +++ b/types/use-positioner.d.ts @@ -13,41 +13,56 @@ import * as React from "react"; * @param options.columnGutter * @param options.rowGutter * @param options.columnCount + * @param options.maxColumnCount */ -export declare function usePositioner({ width, columnWidth, columnGutter, rowGutter, columnCount, }: UsePositionerOptions, deps?: React.DependencyList): Positioner; +export declare function usePositioner( + { + width, + columnWidth, + columnGutter, + rowGutter, + columnCount, + maxColumnCount, + }: UsePositionerOptions, + deps?: React.DependencyList +): Positioner; export interface UsePositionerOptions { - /** - * The width of the container you're rendering the grid within, i.e. the container - * element's `element.offsetWidth` - */ - width: number; - /** - * The minimum column width. The `usePositioner()` hook will automatically size the - * columns to fill their container based upon the `columnWidth` and `columnGutter` values. - * It will never render anything smaller than this width unless its container itself is - * smaller than its value. This property is optional if you're using a static `columnCount`. - * - * @default 200 - */ - columnWidth?: number; - /** - * This sets the horizontal space between grid columns in pixels. If `rowGutter` is not set, this - * also sets the vertical space between cells within a column in pixels. - * - * @default 0 - */ - columnGutter?: number; - /** - * This sets the vertical space between cells within a column in pixels. If not set, the value of - * `columnGutter` is used instead. - */ - rowGutter?: number; - /** - * By default, `usePositioner()` derives the column count from the `columnWidth`, `columnGutter`, - * and `width` props. However, in some situations it is nice to be able to override that behavior - * (e.g. creating a `List` component). - */ - columnCount?: number; + /** + * The width of the container you're rendering the grid within, i.e. the container + * element's `element.offsetWidth` + */ + width: number; + /** + * The minimum column width. The `usePositioner()` hook will automatically size the + * columns to fill their container based upon the `columnWidth` and `columnGutter` values. + * It will never render anything smaller than this width unless its container itself is + * smaller than its value. This property is optional if you're using a static `columnCount`. + * + * @default 200 + */ + columnWidth?: number; + /** + * This sets the horizontal space between grid columns in pixels. If `rowGutter` is not set, this + * also sets the vertical space between cells within a column in pixels. + * + * @default 0 + */ + columnGutter?: number; + /** + * This sets the vertical space between cells within a column in pixels. If not set, the value of + * `columnGutter` is used instead. + */ + rowGutter?: number; + /** + * By default, `usePositioner()` derives the column count from the `columnWidth`, `columnGutter`, + * and `width` props. However, in some situations it is nice to be able to override that behavior + * (e.g. creating a `List` component). + */ + columnCount?: number; + /** + * The upper bound of column count. This property won't work if `columnCount` is set. + */ + maxColumnCount?: number; } /** * Creates a cell positioner for the `useMasonry()` hook. The `usePositioner()` hook uses @@ -59,69 +74,78 @@ export interface UsePositionerOptions { * @param rowGutter - The amount of vertical space between cells within a column in pixels (falls back * to `columnGutter`). */ -export declare const createPositioner: (columnCount: number, columnWidth: number, columnGutter?: number, rowGutter?: number) => Positioner; +export declare const createPositioner: ( + columnCount: number, + columnWidth: number, + columnGutter?: number, + rowGutter?: number +) => Positioner; export interface Positioner { - /** - * The number of columns in the grid - */ - columnCount: number; - /** - * The width of each column in the grid - */ - columnWidth: number; - /** - * Sets the position for the cell at `index` based upon the cell's height - */ - set: (index: number, height: number) => void; - /** - * Gets the `PositionerItem` for the cell at `index` - */ - get: (index: number) => PositionerItem | undefined; - /** - * Updates cells based on their indexes and heights - * positioner.update([index, height, index, height, index, height...]) - */ - update: (updates: number[]) => void; - /** - * Searches the interval tree for grid cells with a `top` value in - * betwen `lo` and `hi` and invokes the callback for each item that - * is discovered - */ - range: (lo: number, hi: number, renderCallback: (index: number, left: number, top: number) => void) => void; - /** - * Returns the number of grid cells in the cache - */ - size: () => number; - /** - * Estimates the total height of the grid - */ - estimateHeight: (itemCount: number, defaultItemHeight: number) => number; - /** - * Returns the height of the shortest column in the grid - */ - shortestColumn: () => number; - /** - * Returns all `PositionerItem` items - */ - all: () => PositionerItem[]; + /** + * The number of columns in the grid + */ + columnCount: number; + /** + * The width of each column in the grid + */ + columnWidth: number; + /** + * Sets the position for the cell at `index` based upon the cell's height + */ + set: (index: number, height: number) => void; + /** + * Gets the `PositionerItem` for the cell at `index` + */ + get: (index: number) => PositionerItem | undefined; + /** + * Updates cells based on their indexes and heights + * positioner.update([index, height, index, height, index, height...]) + */ + update: (updates: number[]) => void; + /** + * Searches the interval tree for grid cells with a `top` value in + * betwen `lo` and `hi` and invokes the callback for each item that + * is discovered + */ + range: ( + lo: number, + hi: number, + renderCallback: (index: number, left: number, top: number) => void + ) => void; + /** + * Returns the number of grid cells in the cache + */ + size: () => number; + /** + * Estimates the total height of the grid + */ + estimateHeight: (itemCount: number, defaultItemHeight: number) => number; + /** + * Returns the height of the shortest column in the grid + */ + shortestColumn: () => number; + /** + * Returns all `PositionerItem` items + */ + all: () => PositionerItem[]; } export interface PositionerItem { - /** - * This is how far from the top edge of the grid container in pixels the - * item is placed - */ - top: number; - /** - * This is how far from the left edge of the grid container in pixels the - * item is placed - */ - left: number; - /** - * This is the height of the grid cell - */ - height: number; - /** - * This is the column number containing the grid cell - */ - column: number; + /** + * This is how far from the top edge of the grid container in pixels the + * item is placed + */ + top: number; + /** + * This is how far from the left edge of the grid container in pixels the + * item is placed + */ + left: number; + /** + * This is the height of the grid cell + */ + height: number; + /** + * This is the column number containing the grid cell + */ + column: number; }