Skip to content

Instantly share code, notes, and snippets.

@guillermodlpa
Last active September 8, 2023 10:21
Show Gist options
  • Save guillermodlpa/87262b0f2f1c8161edad38bdc497ed8f to your computer and use it in GitHub Desktop.
Save guillermodlpa/87262b0f2f1c8161edad38bdc497ed8f to your computer and use it in GitHub Desktop.
React hook to render a responsive CSS grid centering the elements in the last row
import useBreakpointValueWithDeviceType from '@/hooks/useBreakpointValueWithUADetection/useBreakpointValueWithDeviceType';
import { BoxProps } from '@chakra-ui/react';
import { useMemo } from 'react';
// Controlling Leftover Grid Items with Pseudo-selectors
// @see https://css-irl.info/controlling-leftover-grid-items/
function generateCenteredGridStyles(columnCount: number): BoxProps['sx'] {
const itemStyles: Record<string, string | Record<string, string>> = {
gridColumn: 'span 2',
};
if (columnCount >= 2) {
// Dealing with {columnCount} orphan items
for (let i = 1; i < columnCount; i++) {
itemStyles[
`&:nth-last-of-type(${i}):nth-of-type(${columnCount}n - ${i})`
] = {
gridColumnEnd: `-${2 * i}`,
};
}
// Dealing with {columnCount - n} orphan items
if (columnCount > 2) {
for (let i = 1; i < columnCount; i++) {
for (let j = 1; j < columnCount - i; j++) {
itemStyles[
`&:nth-last-of-type(${j}):nth-of-type(${columnCount}n - ${i + j})`
] = {
gridColumnEnd: `-${2 * j + i}`,
};
}
}
}
}
return itemStyles;
}
const generateCenteredGridStylesMemo = memoize(generateCenteredGridStyles);
// double columns, we span each cell double, so we can position them half-way for the last row
// we do this hook to handle responsive number of columns
export default function useResponsiveGridWithLastRowCentering(
countCols:
| number
| { base: number; sm?: number; md?: number; lg?: number; xl?: number },
) {
const countColsResponsive =
typeof countCols === 'number' ? { base: countCols } : countCols;
const { gridTemplateColumns, itemStyleBreakpointValues } = useMemo(() => {
const gridTemplateColumns = {
base: `repeat(${countColsResponsive.base * 2}, 1fr)`,
...(countColsResponsive.sm && {
sm: `repeat(${countColsResponsive.sm * 2}, 1fr)`,
}),
...(countColsResponsive.md && {
md: `repeat(${countColsResponsive.md * 2}, 1fr)`,
}),
...(countColsResponsive.lg && {
lg: `repeat(${countColsResponsive.lg * 2}, 1fr)`,
}),
...(countColsResponsive.xl && {
xl: `repeat(${countColsResponsive.xl * 2}, 1fr)`,
}),
};
const itemStyleBreakpointValues = {
base:
// don't center when it's 2 columns. This is a hardcoded exception in the hook
countColsResponsive.base <= 2
? generateCenteredGridStylesMemo(1)
: generateCenteredGridStylesMemo(countColsResponsive.base),
...(countColsResponsive.sm && {
sm: generateCenteredGridStylesMemo(countColsResponsive.sm),
}),
...(countColsResponsive.md && {
md: generateCenteredGridStylesMemo(countColsResponsive.md),
}),
...(countColsResponsive.lg && {
lg: generateCenteredGridStylesMemo(countColsResponsive.lg),
}),
...(countColsResponsive.xl && {
xl: generateCenteredGridStylesMemo(countColsResponsive.xl),
}),
};
return { gridTemplateColumns, itemStyleBreakpointValues };
}, [
countColsResponsive.base,
countColsResponsive.sm,
countColsResponsive.md,
countColsResponsive.lg,
countColsResponsive.xl,
]);
const itemStyles = useBreakpointValueWithDeviceType(
itemStyleBreakpointValues,
);
return { gridTemplateColumns, itemStyles };
}
// These are examples of the resulting sx descriptors. I had these before I made the generic function
// const itemStylesFourColumns: BoxProps['sx'] = {
// gridColumn: 'span 2',
// // Dealing with 3 orphan items
// '&:nth-last-of-type(1):nth-of-type(4n - 1)': {
// gridColumnEnd: '-2',
// },
// '&:nth-last-of-type(2):nth-of-type(4n - 2)': {
// gridColumnEnd: '-4',
// },
// '&:nth-last-of-type(3):nth-of-type(4n - 3)': {
// gridColumnEnd: '-6',
// },
// // Dealing with 2 orphan items
// '&:nth-last-of-type(1):nth-of-type(4n - 2)': {
// gridColumnEnd: '-3',
// },
// '&:nth-last-of-type(2):nth-of-type(4n - 3)': {
// gridColumnEnd: '-5',
// },
// // Dealing with single orphan item
// '&:nth-last-of-type(1):nth-of-type(4n - 3)': {
// gridColumnEnd: '-4',
// },
// };
// const itemStylesThreeColumns: BoxProps['sx'] = {
// gridColumn: 'span 2',
// // Dealing with 2 orphan items
// '&:nth-last-of-type(1):nth-of-type(3n - 1)': {
// gridColumnEnd: '-2',
// },
// '&:nth-last-of-type(2):nth-of-type(3n - 2)': {
// gridColumnEnd: '-4',
// },
// // Dealing with single orphan item
// '&:nth-last-of-type(1):nth-of-type(3n - 2)': {
// gridColumnEnd: '-3',
// },
// };
// const itemStylesTwoColumns: BoxProps['sx'] = {
// gridColumn: 'span 2',
// // Dealing with single orphan item
// '&:nth-last-of-type(1):nth-of-type(2n - 1)': {
// gridColumnEnd: '-2',
// },
// };
// const itemStylesWheNotCentered: BoxProps['sx'] = {
// gridColumn: 'span 2',
// };
// const itemStylesByNumberOfColumns: Record<number, undefined | BoxProps['sx']> =
// {
// 2: itemStylesTwoColumns,
// 3: itemStylesThreeColumns,
// 4: itemStylesFourColumns,
// };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment