import { memo, useCallback, useDebugValue, useMemo } from 'react'
import styled, { css } from 'styled-components'
import warning from 'warning'
import DotsIcon from '../icons/DotsIcon'
import WedgeIcon, { WedgeDirection } from '../icons/WedgeIcon'
import { useComponentAbstraction } from '../../_lib/ComponentAbstraction'
function usePagerWindow(
page: number,
pageCount: number,
pageRangeDisplayed = 7,
) {
// ページャーのリンク生成例:
//
// < [ 1 ] [*2*] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] >
//
// < [ 1 ] [ 2 ] [ 3 ] [*4*] [ 5 ] [ 6 ] [ 7 ] >
//
// < [ 1 ] ... [ 4 ] [*5*] [ 6 ] [ 7 ] [ 8 ] >
//
// < [ 1 ] ... [ 99 ] [*100*] [ 101 ] [ 102 ] [ 103 ] >
//
// < [ 1 ] ... [ 99 ] [ 100 ] [ 101 ] [ 102 ] [*103*]
//
// [*1*] [ 2 ] >
//
// デザインの意図: 前後移動時のカーソル移動を最小限にする。
if (process.env.NODE_ENV !== 'production') {
warning((page | 0) === page, `\`page\` must be integer (${page})`)
warning(
(pageCount | 0) === pageCount,
`\`pageCount\` must be integer (${pageCount})`,
)
warning(
(pageRangeDisplayed | 0) === pageRangeDisplayed,
`\`pageRangeDisplayed\` must be integer (${pageRangeDisplayed})`,
)
warning(pageRangeDisplayed > 2, `\`windowSize\` must be greater than 2`)
}
const window = useMemo(() => {
const visibleFirstPage = 1
const visibleLastPage = Math.min(
pageCount,
Math.max(page + Math.floor(pageRangeDisplayed / 2), pageRangeDisplayed),
)
if (visibleLastPage <= pageRangeDisplayed) {
// 表示範囲が1-7ページなら省略は無い。
return Array.from(
{ length: 1 + visibleLastPage - visibleFirstPage },
(_, i) => visibleFirstPage + i,
)
} else {
const start = visibleLastPage - (pageRangeDisplayed - 1) + 2
return [
// 表示範囲が1-7ページを超えるなら、
// - 1ページ目は固定で表示する
visibleFirstPage,
// - 2ページ目から現在のページの直前までは省略する
'...' as const,
...Array.from(
{ length: 1 + visibleLastPage - start },
(_, i) => start + i,
),
]
}
}, [page, pageCount, pageRangeDisplayed])
useDebugValue(window)
return window
}
interface CommonProps {
page: number
pageCount: number
pageRangeDisplayed?: number
}
export interface PagerProps extends CommonProps {
onChange(newPage: number): void
}
// this pager is just regular buttons; for links use LinkPager
export default memo(function Pager({
page,
pageCount,
pageRangeDisplayed,
onChange,
}: PagerProps) {
// TODO: refactor Pager and LinkPager to use a common parent component
const window = usePagerWindow(page, pageCount, pageRangeDisplayed)
const makeClickHandler = useCallback(
(value: number) => () => {
onChange(value)
},
[onChange],
)
const hasNext = page < pageCount
const hasPrev = page > 1
return (
{window.map((p) =>
p === '...' ? (
) : p === page ? (
// we remove the onClick but don't mark it as disabled to preserve keyboard focus
// not doing so causes the focus ring to flicker in and out of existence
{p}
) : (
{p}
),
)}
)
})
export interface LinkPagerProps extends CommonProps {
makeUrl(page: number): string
}
export function LinkPager({
page,
pageCount,
pageRangeDisplayed,
makeUrl,
}: LinkPagerProps) {
const { Link } = useComponentAbstraction()
const window = usePagerWindow(page, pageCount, pageRangeDisplayed)
const hasNext = page < pageCount
const hasPrev = page > 1
return (
{window.map((p) =>
p === '...' ? (
) : p === page ? (
{p}
) : (
{p}
),
)}
)
}
const PagerContainer = styled.nav`
display: flex;
justify-content: center;
align-items: center;
`
const CircleButton = styled.button<{ noBackground?: boolean }>`
font-size: 1rem;
line-height: calc(1em + 8px);
text-decoration: none;
border: none;
outline: none;
touch-action: manipulation;
user-select: none;
transition:
box-shadow 0.2s ease 0s,
color 0.2s ease 0s,
background 0.2s ease 0s,
opacity 0.2s ease 0s;
display: flex;
justify-content: center;
align-items: center;
box-sizing: content-box;
min-width: 24px;
min-height: 24px;
padding: 8px;
cursor: pointer;
font-weight: bold;
/* HACK:
* Safari doesn't correctly repaint the elements when they're reordered in response to interaction.
* This forces it to repaint them. This doesn't work if put on the parents either, has to be here.
*/
-webkit-transform: translateZ(0);
&[hidden] {
visibility: hidden;
display: block;
}
border-radius: 48px;
background: transparent;
color: ${({ theme }) => theme.color.text3};
&:hover {
background: ${({ theme }) => theme.color.surface3};
color: ${({ theme }) => theme.color.text2};
}
&[aria-current] {
background-color: ${({ theme }) => theme.color.surface6};
color: ${({ theme }) => theme.color.text5};
}
&[aria-current]:hover {
background-color: ${({ theme }) => theme.color.surface6};
color: ${({ theme }) => theme.color.text5};
}
${({ noBackground = false }) =>
noBackground &&
css`
&:hover {
background: transparent;
}
`}
`
const Spacer = styled(CircleButton).attrs({ type: 'button', disabled: true })`
&& {
color: ${({ theme }) => theme.color.text3};
background: none;
}
`
const Text = 'span'