Last active
May 22, 2023 12:00
-
-
Save Fronix/06560ccef31aee3f29b5a88e2d669048 to your computer and use it in GitHub Desktop.
MUI Pagination hook adjusted for InstantSearch zerobased pagination
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { unstable_useControlled as useControlled } from '@mui/utils'; | |
import type { | |
UsePaginationItem, | |
UsePaginationProps, | |
UsePaginationResult | |
} from './usePaginationTypes'; | |
/** | |
* VERIFIED USING | |
* - [email protected] | |
* - [email protected] | |
* - @meilisearch/[email protected] | |
* | |
* Stolen from https://github.com/mui/material-ui/blob/master/packages/mui-material/src/Pagination/Pagination.js | |
* and modified to work with InstantSearch. | |
* | |
* Types can be found here: https://github.com/mui/material-ui/blob/master/packages/mui-material/src/usePagination/usePagination.d.ts | |
* | |
* Due to InstantSearch being zero-based pagination, we need to adjust the pagination to be zero-based. | |
* | |
* @param props | |
* @returns | |
*/ | |
export default function usePagination(props: UsePaginationProps): UsePaginationResult { | |
// keep default values in sync with @default tags in Pagination.propTypes | |
const { | |
boundaryCount = 1, | |
componentName = 'usePagination', | |
count = 1, | |
defaultPage = 0, | |
disabled = false, | |
hideNextButton = false, | |
hidePrevButton = false, | |
onChange: handleChange, | |
page: pageProp, | |
showFirstButton = false, | |
showLastButton = false, | |
siblingCount = 1, | |
...other | |
} = props; | |
const [page, setPageState] = useControlled({ | |
controlled: pageProp, | |
default: defaultPage, | |
name: componentName, | |
state: 'page' | |
}); | |
const handleClick = (event: any, value: any) => { | |
if (!pageProp) { | |
setPageState(value); | |
} | |
if (handleChange) { | |
handleChange(event, value); | |
} | |
}; | |
// https://dev.to/namirsab/comment/2050 | |
const range = (start: number, end: number) => { | |
const length = end - start + 1; | |
return Array.from({ length }, (_, i) => start + i); | |
}; | |
const startPages = range(0, Math.min(boundaryCount, count)); | |
const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count); | |
const siblingsStart = Math.max( | |
Math.min( | |
// Natural start | |
page - siblingCount, | |
// Lower boundary when page is high | |
count - boundaryCount - siblingCount * 2 - 1 | |
), | |
// Greater than startPages | |
boundaryCount + 2 | |
); | |
const siblingsEnd = Math.min( | |
Math.max( | |
// Natural end | |
page + siblingCount, | |
// Upper boundary when page is low | |
boundaryCount + siblingCount * 2 + 2 | |
), | |
// Less than endPages | |
endPages.length > 0 ? endPages[0] - 2 : count - 1 | |
); | |
// Basic list of items to render | |
// e.g. itemList = ['first', 'previous', 1, 'ellipsis', 4, 5, 6, 'ellipsis', 10, 'next', 'last'] | |
const itemList = [ | |
...(showFirstButton ? ['first'] : []), | |
...(hidePrevButton ? [] : ['previous']), | |
...startPages, | |
// Start ellipsis | |
// eslint-disable-next-line no-nested-ternary | |
...(siblingsStart > boundaryCount + 2 | |
? ['start-ellipsis'] | |
: boundaryCount + 1 < count - boundaryCount | |
? [boundaryCount + 1] | |
: []), | |
// Sibling pages | |
...range(siblingsStart, siblingsEnd), | |
// End ellipsis | |
// eslint-disable-next-line no-nested-ternary | |
...(siblingsEnd < count - boundaryCount - 1 | |
? ['end-ellipsis'] | |
: count - boundaryCount > boundaryCount | |
? [count - boundaryCount] | |
: []), | |
...endPages.slice(0, -1), | |
...(hideNextButton ? [] : ['next']), | |
...(showLastButton ? ['last'] : []) | |
]; | |
// Map the button type to its page number | |
const buttonPage = (type: UsePaginationItem['type']) => { | |
switch (type) { | |
case 'first': | |
return 0; // Default to 0 instead of 1 | |
case 'previous': | |
return page - 1; | |
case 'next': | |
return page + 1; | |
case 'last': | |
return count - 1; // Default to count - 1 so that last page isn't an empty resultspage | |
default: | |
return null; | |
} | |
}; | |
// Convert the basic item list to PaginationItem props objects | |
const items = itemList.map<UsePaginationItem>((item: any) => { | |
return typeof item === 'number' | |
? { | |
onClick: (event) => { | |
handleClick(event, item); | |
}, | |
type: 'page', | |
page: item + 1, // This is only to make the visual pagination 1-based, the onClick is still 0-based | |
selected: item === page, | |
disabled, | |
'aria-current': item === page ? 'true' : undefined | |
} | |
: { | |
onClick: (event) => { | |
handleClick(event, buttonPage(item as UsePaginationItem['type'])); | |
}, | |
type: item, | |
page: buttonPage(item as UsePaginationItem['type']), | |
selected: false, | |
disabled: | |
disabled || | |
(item.indexOf('ellipsis') === -1 && | |
(item === 'next' || item === 'last' ? page >= count : page <= 1)) | |
}; | |
}); | |
return { | |
items, | |
...other | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example usage: