Căn giữa mục được bấm — pattern tab cuộn được
Bấm một tab gần một trong hai cạnh — nó kích hoạt và tự căn giữa trong hàng.
Chỉnh sửa ví dụ này trực tiếp trong Storybook
Cách hoạt động
handleItemClick được curry: nó nhận itemId và trả về một hàm mong đợi đối tượng API. Cú bấm trước tiên lưu id vào state selected, rồi gọi api.getItemElementById(itemId) để tìm phần tử DOM thật và đưa cho api.scrollToItem(item, ’smooth’, ’center’). Một cú bấm, hai hiệu ứng: tab được chọn và căn giữa.
API đến từ đâu
Component cha không bao giờ giữ ref API. Mỗi Card đọc toàn bộ API từ VisibilityContext — sẵn cho bất kỳ con nào của ScrollMenu — và truyền nó vào handler bấm: onClick(visibility). Nếu thay vào đó bạn cần cuộn từ ngoài menu, đó là pattern apiRef trong ví dụ scroll-to-item.
Ghi chú
- Đối số thứ ba của
scrollToItemnhận cùng các giá trị như tùy chọninlinecủascrollIntoView—’start’,’center’hoặc’end’. - Các thẻ có thể nhận focus (
role="button",tabIndex=0) và xử lý Enter trongonKeyDown, nên người dùng bàn phím nhận cùng thao tác chọn-và-căn-giữa. - Handler
onWheelánh xạ delta con lăn chuột sangscrollNext/scrollPrev, nhưng nhường cho touchpad — delta ngang hoặc delta dọc rất nhỏ được coi là cử chỉ và để gốc. - Các mũi tên tự vô hiệu hóa bằng các dạng ngắn
useIsVisible(’first’)vàuseIsVisible(’last’).
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';
export function CenterOnClick() {
const [items] = React.useState(() => getItems());
const [selected, setSelected] = React.useState<string>('');
const isItemSelected = (id: string): boolean => selected === id;
// The Card reads the api from VisibilityContext and passes it here,
// so a single click both selects the item and centers it.
const handleItemClick = (itemId: string) => (api: publicApiType) => {
setSelected(itemId);
const item = api.getItemElementById(itemId);
if (item) {
api.scrollToItem(item, 'smooth', 'center');
}
};
return (
<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>
);
}
export default CenterOnClick;
function LeftArrow() {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isFirstItemVisible = visibility.useIsVisible('first', true);
return (
<Arrow
disabled={isFirstItemVisible}
onClick={() => visibility.scrollPrev()}
testId="left-arrow"
>
Left
</Arrow>
);
}
function RightArrow() {
const visibility = React.useContext<publicApiType>(VisibilityContext);
const isLastItemVisible = visibility.useIsVisible('last', false);
return (
<Arrow
disabled={isLastItemVisible}
onClick={() => visibility.scrollNext()}
testId="right-arrow"
>
Right
</Arrow>
);
}
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(14)
.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();
}
}