Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { VoidFunctionComponent } from 'react';
import { StyledIconButton } from './styled';
import { usePaginationContext } from '../../context';
import { useDeviceContext } from '../../../device-context-provider/device-context-provider';

export type NavigationAction = 'previous' | 'next';

export interface PaginationNavButtonProps {
action: NavigationAction;
}

export const PaginationNavButton: VoidFunctionComponent<PaginationNavButtonProps> = (
{ action },
) => {
const { isMobile } = useDeviceContext();
const {
totalPages,
pagesDisplayed,
currentPage,
changePage,
} = usePaginationContext();
const showNavButtons = totalPages > 3 || pagesDisplayed < totalPages;

if (!showNavButtons) return null;

const isPrevious = action === 'previous';
const isDisabled = isPrevious ? currentPage <= 1 : currentPage >= totalPages;
const onClick = (): void => changePage(currentPage + (isPrevious ? -1 : 1));
const iconName = isPrevious ? 'chevronLeft' : 'chevronRight';
const label = isPrevious ? 'previous page' : 'next page';
const dataTestId = isPrevious ? 'previousPageButton' : 'nextPageButton';

return (
<StyledIconButton
data-testid={dataTestId}
className={`pagination-navigation-button-${isPrevious ? 'previous' : 'next'}`}
iconName={iconName}
label={label}
type="button"
buttonType="tertiary"
disabled={isDisabled}
onClick={onClick}
$isMobile={isMobile}
$isVisible={!isDisabled}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import styled from 'styled-components';
import { IconButton } from '../../../buttons';

export const StyledIconButton = styled(IconButton)<{ $isVisible: boolean, $isMobile: boolean }>`
align-items: center;
display: flex;
gap: 0.5rem;
min-height: ${({ $isMobile }) => ($isMobile ? 'var(--size-2x)' : 'var(--size-1halfx)')};
min-width: ${({ $isMobile }) => ($isMobile ? 'var(--size-2x)' : 'var(--size-1halfx)')};
visibility: ${({ $isVisible }) => ($isVisible ? 'visible' : 'hidden')};
width: auto;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FC, useCallback } from 'react';
import { useDeviceContext } from '../../../device-context-provider/device-context-provider';
import { PageButtonWrapper, StyledButton, StyledText } from './styled';
import { usePaginationContext } from '../../context';

interface PageButtonProps {
index: number;
}

export const PaginationPageButton: FC<PageButtonProps> = ({
index,
}) => {
const { isMobile } = useDeviceContext();
const { currentPage, changePage } = usePaginationContext();
const isSelected = index === currentPage;
const id = `page-${index}`;

const handlePageKeyDown = useCallback((key: string, page: number): void => {
if (key === 'Enter') {
changePage(page);
}
}, [changePage]);

return (
<PageButtonWrapper key={id}>
<StyledButton
aria-label={`go to page ${index}`}
aria-current={isSelected ? 'page' : undefined}
className="pagination-page-button"
data-testid={id}
$isSelected={isSelected}
onClick={isSelected ? undefined : () => changePage(index)}
onKeyPress={(event) => handlePageKeyDown(event.key, index)}
type="button"
$isMobile={isMobile}
>
<StyledText
$isMobile={isMobile}
$isSelected={isSelected}
>
{index}
</StyledText>
</StyledButton>
</PageButtonWrapper>

);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import styled from 'styled-components';
import { focus } from '../../../../utils/css-state';

type SelectionSuffix = '-selected' | '';

function getSelectionSuffix(isSelected: boolean): SelectionSuffix {
return isSelected ? '-selected' : '';
}

export const PageButtonWrapper = styled.li`
list-style: none;
`;

export const StyledButton = styled.button<{ $isSelected: boolean, $isMobile: boolean }>`
align-items: center;
background-color: ${({ $isSelected, theme }) => theme.component[`pagination-page${getSelectionSuffix($isSelected)}-background-color`]};
border: ${({ $isSelected, theme }) => ($isSelected ? `1px solid ${theme.component['pagination-page-selected-border-color']}` : 'none')};
border-radius: var(--border-radius-4x);
display: flex;
justify-content: center;
max-height: ${({ $isMobile }) => ($isMobile ? 'var(--size-2x)' : 'var(--size-1halfx)')};
min-width: ${({ $isMobile }) => ($isMobile ? 'var(--size-2x)' : 'var(--size-1halfx)')};
padding: ${({ $isMobile }) => ($isMobile ? '0.25rem 0.5rem' : '0.125rem 0.25rem')};
Comment thread
hebernardEquisoft marked this conversation as resolved.
user-select: none;

${focus};

&:hover {
background-color: ${({ $isSelected, theme }) => theme.component[`pagination-page${getSelectionSuffix($isSelected)}-hover-background-color`]};
}
`;

export const StyledText = styled.span<{ $isSelected: boolean, $isMobile: boolean }>`
color: ${({ $isSelected, theme }) => theme.component[`pagination-page${getSelectionSuffix($isSelected)}-text-color`]};
font-size: ${({ $isMobile }) => ($isMobile ? 1 : 0.875)}rem;
font-weight: ${({ $isSelected }) => ($isSelected ? 'var(--font-bold)' : 'var(--font-normal)')};
letter-spacing: 0.0125rem;
line-height: ${({ $isMobile }) => ($isMobile ? 1.5 : 1.25)}rem;
Comment thread
hebernardEquisoft marked this conversation as resolved.

&:hover {
${({ $isSelected, theme }) => !$isSelected && `
color: ${theme.component['pagination-page-hover-text-color']};
`}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FC } from 'react';
import { useDeviceContext } from '../../device-context-provider/device-context-provider';
import { usePaginationContext } from '../context';
import { range } from '../../../utils/range';
import { calculateShownPageRange } from '../util/pagination-util';
import { PaginationPageButton } from './page-button/pagination-page-button';
import { PaginationNavButton } from './nav-button/pagination-nav-button';
import { PaginationButtonsWrapper, PaginationPageButtonsWrapper } from './styled';

export const PaginationButtons: FC = () => {
const { isMobile } = useDeviceContext();
const { currentPage, totalPages, pagesDisplayed } = usePaginationContext();
const { begin, end } = calculateShownPageRange(totalPages, pagesDisplayed, currentPage);

if (totalPages <= 1) {
return null;
}

return (
<PaginationButtonsWrapper
className="pagination-buttons-wrapper"
$isMobile={isMobile}
>
<PaginationNavButton action="previous" />
<PaginationPageButtonsWrapper
className="pagination-page-buttons-wrapper"
$isMobile={isMobile}
>
{range(begin, end).map((i) => (
<PaginationPageButton key={i} index={i} />
))}
</PaginationPageButtonsWrapper>
<PaginationNavButton action="next" />
</PaginationButtonsWrapper>

);
};
16 changes: 16 additions & 0 deletions packages/react/src/components/pagination/buttons/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled from 'styled-components';

export const PaginationButtonsWrapper = styled.div<{ $isMobile: boolean }>`
align-items: center;
display: flex;
gap: ${({ $isMobile }) => ($isMobile ? '0.75rem' : '0.5rem')};
padding: 0;
`;

export const PaginationPageButtonsWrapper = styled.ol<{ $isMobile: boolean }>`
align-items: center;
display: flex;
gap: ${({ $isMobile }) => ($isMobile ? '0.75rem' : '0.5rem')};
margin: 0;
padding: 0;
`;
24 changes: 24 additions & 0 deletions packages/react/src/components/pagination/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createContext, useContext } from 'react';

export interface PaginationContextProps {
resultsPerPage: number;
numberOfResults: number;
totalPages: number;
pagesDisplayed: number;
currentPage: number;

/**
* Function callback to change page
*/
changePage: (pageNumber: number) => void;
}

export const PaginationContext = createContext<PaginationContextProps | undefined>(undefined);

export function usePaginationContext(): PaginationContextProps {
const context = useContext(PaginationContext);
if (!context) {
throw new Error('usePaginationContext must be used within a PaginationProvider');
}
return context;
}
2 changes: 2 additions & 0 deletions packages/react/src/components/pagination/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Pagination } from './pagination';
export { PaginationProps } from './pagination';
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FC } from 'react';
import { ScreenReaderOnlyText } from '../../screen-reader-only-text/ScreenReaderOnlyText';
import { useTranslation } from '../../../i18n/use-translation';
import { useDeviceContext } from '../../device-context-provider/device-context-provider';
import { ResultsLabelHeading } from './styled';
import { usePaginationContext } from '../context';

interface PageLabelProps {
id: string;
}

export const PaginationLabel: FC<PageLabelProps> = ({
id,
}) => {
const { t } = useTranslation('pagination');
const { isMobile } = useDeviceContext();
const { resultsPerPage, currentPage, numberOfResults } = usePaginationContext();

const pageStartIndex = resultsPerPage && numberOfResults
? (resultsPerPage * (currentPage - 1)) + 1 : 0;
const pageEndIndex = resultsPerPage && numberOfResults
? Math.min(numberOfResults, resultsPerPage * currentPage) : 0;

return (
<div className="pagination-label" aria-live='off' role='status'>
<ResultsLabelHeading
id={id}
data-testid="resultsLabel"
type="small"
tag="h3"
$isMobile={isMobile}
>
<ScreenReaderOnlyText label={`${t('pagination')} - `} />
{t('results', {
pageStartIndex,
pageEndIndex,
numberOfResults,
})}
</ResultsLabelHeading>
</div>
);
};
10 changes: 10 additions & 0 deletions packages/react/src/components/pagination/label/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from 'styled-components';
import { Heading } from '../../heading/heading';

export const ResultsLabelHeading = styled(Heading)<{ $isMobile: boolean }>`
font-size: ${({ $isMobile }) => ($isMobile ? 1 : 0.875)}rem;
font-weight: var(--font-normal);
letter-spacing: 0.0125rem;
line-height: ${({ $isMobile }) => ($isMobile ? 1.5 : 1.25)}rem;
margin: 0;
`;
Loading