Một mục mỗi view: slider toàn chiều rộng từ cùng một menu
min-width: 100% trên bao bọc mục của thư viện — biến cùng component đó thành slider: mỗi thẻ lấp đầy view, và các mũi tên phân trang thường tiến đúng một mục.Phân trang bằng mũi tên — mỗi slide đúng một view rộng, và mỗi slide báo khả năng hiển thị riêng.
Chỉnh sửa ví dụ này trực tiếp trong Storybook
Cách hoạt động
Story bọc menu trong một container có style nhắm tới .react-horizontal-scrolling-menu--item — div mà thư viện render quanh mỗi con — và cho nó minWidth: ’100%’ cộng căn giữa flex. Giờ mỗi bao bọc trải khắp toàn container cuộn, nên một thẻ vừa đầy view. Các mũi tên là chuẩn: scrollPrev() và scrollNext() phân trang theo nhóm hiển thị, và khi nhóm hiển thị là một mục, một trang và một mục là một.
Mũi tên và con lăn
Trạng thái mũi tên đến từ useLeftArrowVisible() và useRightArrowVisible() — mỗi cái trả true khi hàng ở đầu đó, và story đưa nó vào disabled và làm mờ nút. Prop onWheel nhận đối tượng API cùng sự kiện, nên con lăn dọc phân trang hàng theo dấu của deltaY. Trước hết nó đánh hơi touchpad: bất kỳ delta ngang nào, hoặc delta dọc dưới 15, được coi là cử chỉ touchpad và để cho cuộn gốc.
Ghi chú
itemIdtrên mỗi con là yêu cầu cứng duy nhất — đó là cách các mục được theo dõi và cuộn tới.- Các thẻ vẫn gọi
useIsVisible(itemId, true); với một mục mỗi view, mọi slide ngoài màn hình báovisible: false. - Thanh cuộn được ẩn bằng CSS thuần trên container cuộn (
scrollbar-width: nonecộng pseudo-element WebKit) — lựa chọn đó là của bạn, không phải của thư viện. - Chiều rộng nằm hoàn toàn trong stylesheet của bạn. Đổi 100% thành 50% và bạn có slider hai-mỗi-view; thư viện không đo gì.
Nguồn đầy đủ
Đầy đủ và sẵn sàng sao chép-dán — đây là file chính xác phía sau phiên bản Storybook có thể chỉnh sửa trực tiếp.
import 'react-horizontal-scrolling-menu/dist/styles.css';
import styled from '@emotion/styled';
import React from 'react';
import {
type publicApiType,
ScrollMenu,
VisibilityContext,
} from 'react-horizontal-scrolling-menu';
const WideItems = styled('div')({
'& .react-horizontal-scrolling-menu--item ': {
minWidth: '100%',
display: 'flex',
justifyContent: 'center',
},
});
export function OneItem() {
const [items] = React.useState(() => getItems());
const [selected, setSelected] = React.useState<string[]>([]);
const isItemSelected = (id: string): boolean =>
!!selected.find((el) => el === id);
const handleItemClick = (itemId: string) => {
const itemSelected = isItemSelected(itemId);
setSelected((currentSelected: string[]) =>
itemSelected
? currentSelected.filter((el) => el !== itemId)
: currentSelected.concat(itemId),
);
};
return (
<WideItems>
<NoScrollbar>
<ScrollMenu
LeftArrow={LeftArrow}
RightArrow={RightArrow}
onWheel={onWheel}
>
{items.map(({ id }) => (
<Card
title={id}
itemId={id} // NOTE: itemId is required for track items
key={id}
onClick={() => handleItemClick(id)}
selected={isItemSelected(id)}
/>
))}
</ScrollMenu>
</NoScrollbar>
</WideItems>
);
}
export default OneItem;
function LeftArrow() {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const disabled = visibility.useLeftArrowVisible();
return (
<Arrow
disabled={disabled}
onClick={() => visibility.scrollPrev()}
testId="left-arrow"
>
Left
</Arrow>
);
}
function RightArrow() {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const disabled = visibility.useRightArrowVisible();
return (
<Arrow
disabled={disabled}
onClick={() => visibility.scrollNext()}
testId="right-arrow"
>
Right
</Arrow>
);
}
const NoScrollbar = styled('div')({
'& .react-horizontal-scrolling-menu--scroll-container::-webkit-scrollbar': {
display: 'none',
},
'& .react-horizontal-scrolling-menu--scroll-container': {
scrollbarWidth: 'none',
'-ms-overflow-style': 'none',
},
});
function Arrow({
children,
disabled,
onClick,
className,
testId,
}: {
children: React.ReactNode;
disabled: boolean;
onClick: VoidFunction;
className?: string;
testId: string;
}) {
return (
<ArrowButton
disabled={disabled}
onClick={onClick}
className={'arrow' + `-${className}`}
data-testid={testId}
>
{children}
</ArrowButton>
);
}
const ArrowButton = styled('button')((props) => ({
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
marginBottom: '2px',
opacity: props.disabled ? '0' : '1',
userSelect: 'none',
borderRadius: '6px',
borderWidth: '1px',
}));
function Card({
onClick,
selected,
title,
itemId,
}: {
onClick: (context: publicApiType) => void;
selected: boolean;
title: string;
itemId: string;
}) {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isVisible = visibility.useIsVisible(itemId, true);
return (
<CardBody
data-cy={itemId}
onClick={() => onClick(visibility)}
onKeyDown={(ev: React.KeyboardEvent) => {
ev.code === 'Enter' && onClick(visibility);
}}
data-testid="card"
role="button"
tabIndex={0}
className="card"
visible={isVisible}
selected={selected}
>
<div className="header">
<div>{title}</div>
<div className="visible">visible: {JSON.stringify(isVisible)}</div>
<div className="selected">selected: {JSON.stringify(!!selected)}</div>
</div>
<div className="background" />
</CardBody>
);
}
const CardBody = styled('div')<{ selected?: boolean; visible?: boolean }>(
(props) => ({
border: '1px solid',
display: 'inline-block',
margin: '0 10px',
width: '160px',
userSelect: 'none',
borderRadius: '8px',
overflow: 'hidden',
'& .header': {
backgroundColor: 'white',
},
'& .visible': {
backgroundColor: props.visible ? 'transparent' : 'gray',
},
'& .background': {
backgroundColor: props.selected ? 'green' : 'bisque',
height: '200px',
},
}),
);
const getId = (index: number) => `${'test'}${index}`;
const getItems = () =>
Array(10)
.fill(0)
.map((_, ind) => ({ id: getId(ind) }));
function onWheel(apiObj: publicApiType, ev: React.WheelEvent): void {
// NOTE: no good standart way to distinguish touchpad scrolling gestures
// but can assume that gesture will affect X axis, mouse scroll only Y axis
// of if deltaY too small probably is it touchpad
const isThouchpad = Math.abs(ev.deltaX) !== 0 || Math.abs(ev.deltaY) < 15;
if (isThouchpad) {
ev.stopPropagation();
return;
}
if (ev.deltaY < 0) {
apiObj.scrollNext();
} else {
apiObj.scrollPrev();
}
}