Skip to content
108 changes: 108 additions & 0 deletions src/components/PaginationOne/PageNumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useState, useRef, useEffect, useCallback, forwardRef, memo } from 'react';

import { MenuActionsPanel } from '#src/components/Menu/MenuActionsPanel';
import { TextInput } from '#src/components/input';
import { keyboardKey } from '#src/components/common/keyboardKey.js';
import type { PaginationOneProps } from '#src/components/PaginationOne';
import type { MenuDimensions } from '#src/components/Menu';
import { refSetter } from '#src/components/common/utils/refSetter';

type PageNumberInputProps = {
page: PaginationOneProps['page'];
pageSize: PaginationOneProps['pageSize'];
totalPages: number;
dimension: MenuDimensions;
activePageNumber: string | undefined;
setActivePageNumber: (value: string) => void;
setMenuVisible: (isVisible: boolean) => void;
onChange: PaginationOneProps['onChange'];
};

export const PageNumberInput = memo(
forwardRef<HTMLInputElement, PageNumberInputProps>(
(
{ dimension, page, pageSize, totalPages, activePageNumber, setActivePageNumber, setMenuVisible, onChange },
ref,
) => {
/**
* При монтировании инпута важно, чтобы его значение было равно выбранной странице (page).
* Дальнейшая синхронизация значений не нужна, так как при выборе новой страницы
* дропдаун с инпутом будут закрыты.
**/
const [inputPageNumber, setInputPageNumber] = useState(page.toString());
const pageNumberInputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
pageNumberInputRef.current?.select();
pageNumberInputRef.current?.focus();
}, []);

useEffect(() => {
/** Если activePageNumber обновилось независимо от инпута,
* например, при срабатывании onActivateItem,
* нужно синхронизировать inputPageNumber с новым значением активной опции */
if (activePageNumber && inputPageNumber !== activePageNumber) {
setInputPageNumber(activePageNumber);
}
}, [activePageNumber]);

const parsePageNumber = useCallback(
(pageSelected: string): number => {
if (pageSelected === '') {
return page;
}
const pageSelectedNumber = Number.parseInt(pageSelected, 10);
if (Number.isNaN(pageSelectedNumber) || pageSelectedNumber < 1) {
return 1;
}
if (pageSelectedNumber > totalPages) {
return totalPages;
}
return pageSelectedNumber;
},
[page, totalPages],
);

const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
let inputValue = e.target.value;
inputValue = inputValue.replace(/\D/g, '');
setInputPageNumber(inputValue);
setActivePageNumber(parsePageNumber(inputValue).toString());
},
[parsePageNumber],
);

const handleInputKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const code = keyboardKey.getCode(e);

if (code === keyboardKey.Enter) {
setMenuVisible(false);
const page = parsePageNumber(inputPageNumber);
onChange({ page, pageSize });
setActivePageNumber(page.toString());
} else if (code === keyboardKey.ArrowDown || code === keyboardKey.ArrowUp) {
pageNumberInputRef.current?.blur();
} else if (code === keyboardKey.Escape) {
setMenuVisible(false);
setActivePageNumber(page.toString());
}
},
[inputPageNumber, onChange, page, pageSize],
);

return (
<MenuActionsPanel dimension={dimension}>
<TextInput
ref={refSetter(ref, pageNumberInputRef)}
dimension="s"
value={inputPageNumber}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
/>
</MenuActionsPanel>
);
},
),
);
151 changes: 151 additions & 0 deletions src/components/PaginationOne/PageSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { useTheme, css } from 'styled-components';
import { memo, useState, useMemo, useRef, useEffect, useCallback } from 'react';

import { LIGHT_THEME } from '#src/components/themes';
import type { PaginationOneProps } from '#src/components/PaginationOne';
import { passDropdownDataAttributes } from '#src/components/common/utils/splitDataAttributes';

import { MenuButton } from './Menu';
import { PageNumberInput } from './PageNumberInput';

const extendMixin = (mixin?: ReturnType<typeof css>, showPageNumberInput?: boolean) => css`
width: auto;
min-width: ${showPageNumberInput ? 80 : 68}px;

${mixin};
`;

interface PageSelectProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
dimension: PaginationOneProps['dimension'];
page: PaginationOneProps['page'];
pageSize: PaginationOneProps['pageSize'];
pageSelectDisabled: PaginationOneProps['pageSelectDisabled'];
totalPages: number;
localeLabel: ((...props: any) => string) | undefined;
onChange: PaginationOneProps['onChange'];
dropMaxHeight: string | number | undefined;
menuWidth: string | undefined;
pageNumberDropContainerStyle: PaginationOneProps['pageNumberDropContainerStyle'];
dropContainerCssMixin: PaginationOneProps['dropContainerCssMixin'];
showPageNumberInput: boolean;
}

export const PageSelect = memo(
({
page,
pageSize,
pageSelectDisabled,
totalPages,
localeLabel,
dimension,
onChange,
dropMaxHeight,
menuWidth,
pageNumberDropContainerStyle,
dropContainerCssMixin,
showPageNumberInput,
...props
}: PageSelectProps) => {
const theme = useTheme() || LIGHT_THEME;
const { pageSelectLabel: theme_pageSelectLabel } = theme.locales[theme.currentLocale].paginationOne;
const pageSelectLabel = localeLabel || theme_pageSelectLabel;

const [isVisible, setIsVisible] = useState(false);
const [activePageNumber, setActivePageNumber] = useState<string | undefined>(page.toString());
const pageNumberInputRef = useRef<HTMLInputElement>(null);

const dropMenuProps = passDropdownDataAttributes(props);
const pages = useMemo(() => Array.from({ length: totalPages }, (_, k) => k + 1), [totalPages]);
const selectedPageNumber = useMemo(() => page.toString(), [page]);

useEffect(() => {
setActivePageNumber(page.toString());
}, [page]);

const handlePageHover = useCallback((activePage?: string) => {
if (activePage) {
setActivePageNumber(activePage);
}
}, []);

const handlePageChange = useCallback(
(pageSelected: string) => {
const page = Number.parseInt(pageSelected, 10);
onChange({
page,
pageSize,
});
setIsVisible(false);
},
[onChange, pageSize],
);

const handleClickOutside = useCallback(() => {
setActivePageNumber(selectedPageNumber);
setIsVisible(false);
}, [selectedPageNumber]);

const handleMenuButtonClick = useCallback(() => {
if (isVisible) {
setActivePageNumber(selectedPageNumber);
setIsVisible(false);
} else {
setIsVisible(true);
}
}, [isVisible, selectedPageNumber]);

const handleMenuCycle = useCallback(() => {
pageNumberInputRef.current?.focus();
return false;
}, []);

return (
<MenuButton
dimension={dimension}
options={pages}
selected={selectedPageNumber}
onSelectItem={handlePageChange}
active={activePageNumber}
onActivateItem={handlePageHover}
disabled={pageSelectDisabled}
aria-label={pageSelectLabel(page, totalPages)}
menuMaxHeight={pageNumberDropContainerStyle?.menuMaxHeight || dropMaxHeight}
dropContainerCssMixin={
pageNumberDropContainerStyle?.dropContainerCssMixin || extendMixin(dropContainerCssMixin, showPageNumberInput)
}
dropContainerClassName={pageNumberDropContainerStyle?.dropContainerClassName}
dropContainerStyle={pageNumberDropContainerStyle?.dropContainerStyle}
menuWidth={pageNumberDropContainerStyle?.menuWidth || menuWidth}
dropMenuDataAttributes={dropMenuProps}
className="current-page-number-with-dropdown"
isVisible={isVisible}
onVisibilityChange={setIsVisible}
onClickOutside={handleClickOutside}
onClick={handleMenuButtonClick}
onForwardCycleApprove={handleMenuCycle}
onBackwardCycleApprove={handleMenuCycle}
renderTopPanel={
showPageNumberInput
? ({ dimension = 's' }) => {
return (
<PageNumberInput
ref={pageNumberInputRef}
dimension={dimension}
page={page}
pageSize={pageSize}
totalPages={totalPages}
activePageNumber={activePageNumber}
setActivePageNumber={setActivePageNumber}
setMenuVisible={setIsVisible}
onChange={onChange}
/>
);
}
: undefined
}
>
{page}
</MenuButton>
);
},
);
77 changes: 77 additions & 0 deletions src/components/PaginationOne/PageSizeSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useTheme, css } from 'styled-components';
import { memo } from 'react';

import { LIGHT_THEME } from '#src/components/themes';
import type { PaginationOneProps } from '#src/components/PaginationOne';
import { passDropdownDataAttributes } from '#src/components/common/utils/splitDataAttributes';

import { MenuButton } from './Menu';

const extendMixin = (mixin?: ReturnType<typeof css>) => css`
width: auto;
min-width: 68px;

${mixin};
`;

interface PageSizeSelectProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
dimension: PaginationOneProps['dimension'];
pageSize: PaginationOneProps['pageSize'];
pageSizes: PaginationOneProps['pageSizes'];
pageSizeSelectDisabled: PaginationOneProps['pageSizeSelectDisabled'];
localeLabel: ((...props: any) => string) | undefined;
totalItems: PaginationOneProps['totalItems'];
onChange: PaginationOneProps['onChange'];
dropMaxHeight: string | number | undefined;
menuWidth: string | undefined;
pageSizeDropContainerStyle: PaginationOneProps['pageSizeDropContainerStyle'];
dropContainerCssMixin: PaginationOneProps['dropContainerCssMixin'];
}

export const PageSizeSelect = memo(
({
pageSize,
pageSizes,
pageSizeSelectDisabled,
localeLabel,
dimension,
totalItems,
onChange,
dropMaxHeight,
menuWidth,
pageSizeDropContainerStyle,
dropContainerCssMixin,
...props
}: PageSizeSelectProps) => {
const theme = useTheme() || LIGHT_THEME;
const { pageSizeSelectLabel: theme_pageSizeSelectLabel } = theme.locales[theme.currentLocale].paginationOne;
const pageSizeSelectLabel = localeLabel || theme_pageSizeSelectLabel;

const dropMenuProps = passDropdownDataAttributes(props);

const handleSizeChange = (pageSizeSelected: string) => {
const pageSize = Number.parseInt(pageSizeSelected, 10);
onChange({ page: 1, pageSize: pageSize });
};

return (
<MenuButton
dimension={dimension}
options={pageSizes}
selected={pageSize.toString()}
onSelectItem={handleSizeChange}
disabled={pageSizeSelectDisabled}
aria-label={pageSizeSelectLabel(pageSize, totalItems)}
menuMaxHeight={pageSizeDropContainerStyle?.menuMaxHeight || dropMaxHeight}
menuWidth={pageSizeDropContainerStyle?.menuWidth || menuWidth}
dropContainerCssMixin={pageSizeDropContainerStyle?.dropContainerCssMixin || extendMixin(dropContainerCssMixin)}
dropContainerClassName={pageSizeDropContainerStyle?.dropContainerClassName}
dropContainerStyle={pageSizeDropContainerStyle?.dropContainerStyle}
dropMenuDataAttributes={dropMenuProps}
className="records-per-page-with-dropdown"
>
{pageSize}
</MenuButton>
);
},
);
Loading