|
import React from 'react' |
|
import { View, useDripsyTheme, useResponsiveValue, Theme as DripsyTheme } from 'dripsy' |
|
import { pickChild } from './pick-child' |
|
|
|
type Props = { |
|
/** |
|
* Number of columns that should appear. If you want different columns at different screen sizes, use an array of numbers. |
|
*/ |
|
columns?: number | number[] |
|
children: React.ReactNode |
|
/** |
|
* A number in pixels, or a theme value, such as `$3`. If you're using an old version of Dripsy that allowed numbers as theme keys (such as from an array), you can pass `"14px"` instead of `14`. |
|
*/ |
|
gap?: |
|
| (number | (string & {}) | keyof DripsyTheme['space']) |
|
| Array<(number | (string & {})) | keyof DripsyTheme['space']> |
|
sx?: React.ComponentProps<typeof View>['sx'] |
|
} |
|
|
|
function GridItem({ children, ...sx }: { children: React.ReactNode }) { |
|
return <View sx={sx as any}>{children}</View> |
|
} |
|
|
|
export default function Grid(props: Props) { |
|
const { sx = {}, children, gap: _gap = 3, ...viewProps } = props |
|
|
|
const gap = useResponsiveValue(Array.isArray(_gap) ? _gap : [_gap]) |
|
|
|
const { space } = useDripsyTheme().theme as any as DripsyTheme |
|
|
|
const columnStyle = (numberOfColumns: number, index: number) => { |
|
const getGap = (gap: string | number) => { |
|
let rawPadding = 0 |
|
if (typeof gap === 'string' && gap.endsWith('px')) { |
|
rawPadding = Number(gap.replace('px', '')) + 0.0000001 |
|
} else if (typeof gap === 'string') { |
|
rawPadding = space[gap] |
|
} else if (typeof gap === 'number') { |
|
rawPadding = space[gap] ?? gap |
|
} |
|
// we need to turn this into a raw value to avoid conflicts with dripsy themes |
|
// for instance, if rawPadding / 2 = 4, and we have theme.space[4] = 8 |
|
// it'll incorrectly use 8. By adding .0000001 etc, we hack our way to ensuring we use 4px |
|
// TODO once Dripsy has "10px" feature added, use that instead |
|
const horizontalRawPadding = rawPadding / 2 + 0.000000001 |
|
const topRawPadding = rawPadding + 0.000001 |
|
|
|
return { |
|
horizontalRawPadding, |
|
topRawPadding, |
|
} |
|
} |
|
|
|
let horizontalRawPadding: string | number | string[] | number[] = 0 |
|
let topRawPadding: string | number | string[] | number[] = 0 |
|
|
|
;({ horizontalRawPadding, topRawPadding } = getGap(gap)) |
|
|
|
const isLastItemInRow = (index + 1) % numberOfColumns === 0 |
|
const isFirstItemInRow = index % numberOfColumns === 0 |
|
|
|
const isAfterFirstRow = index + 1 > numberOfColumns |
|
|
|
return { |
|
paddingLeft: isFirstItemInRow ? 0 : horizontalRawPadding, |
|
paddingRight: isLastItemInRow ? 0 : horizontalRawPadding, |
|
width: `${(1 / numberOfColumns) * 100}%`, |
|
paddingTop: isAfterFirstRow ? topRawPadding : 0, |
|
} |
|
} |
|
|
|
const [, gridItemChildren] = pickChild(children, GridItem) |
|
const gridItemChildrenCount = React.Children.count(gridItemChildren) |
|
|
|
const { |
|
// default the number of columns to the number of children, with reasonable limits |
|
columns = [ |
|
Math.min(gridItemChildrenCount, 2), // mobile defaults to max 2 items per row |
|
Math.min(gridItemChildrenCount, 3), // tablet defaults to max 3 items per row |
|
Math.min(gridItemChildrenCount, 3), // laptop defaults to max 3 items per row |
|
Math.min(gridItemChildrenCount, 4), // desktop defaults to max 4 items per row |
|
], |
|
} = props |
|
|
|
return ( |
|
<View {...viewProps} sx={{ flexDirection: 'row', flexWrap: 'wrap', ...sx }}> |
|
{React.Children.map(children, (child: React.ReactElement, index) => { |
|
let paddingLeft: number | number[] = 0 |
|
let paddingRight: number | number[] = 0 |
|
let paddingTop: number | number[] = 0 |
|
let width: string | string[] = '100%' |
|
|
|
if (typeof columns === 'number') { |
|
;({ paddingLeft, paddingRight, width, paddingTop } = columnStyle( |
|
columns, |
|
index |
|
)) |
|
} else { |
|
const mappedColumns = columns.map((columnCount) => |
|
columnStyle(columnCount, index) |
|
) |
|
const reducedColumns = mappedColumns.reduce( |
|
(acc, { paddingRight, paddingLeft, width, paddingTop }) => { |
|
return { |
|
// set the responsive width for this column |
|
width: [...acc.width, width], |
|
|
|
// these values don't need to be accumulated, since they're the same for a given column |
|
paddingLeft: [...acc.paddingLeft, paddingLeft], |
|
paddingRight: [...acc.paddingRight, paddingRight], |
|
paddingTop: [...acc.paddingTop, paddingTop], |
|
} |
|
}, |
|
{ |
|
paddingLeft: [], |
|
paddingRight: [], |
|
width: [], |
|
paddingTop: [], |
|
} |
|
) |
|
;({ paddingRight, paddingLeft, width, paddingTop } = reducedColumns) |
|
} |
|
if (!child) return null |
|
return React.cloneElement(child, { |
|
...child.props, |
|
pt: paddingTop, |
|
pr: paddingRight, |
|
width, |
|
pl: paddingLeft, |
|
}) |
|
})} |
|
</View> |
|
) |
|
} |
|
|
|
Grid.Item = GridItem |