화면당 항목 하나: 같은 메뉴에서 나온 전체 너비 슬라이더

켜야 할 슬라이더 모드는 없습니다. 메뉴는 CSS가 말하는 대로 배치하므로, 한 규칙 — 라이브러리 항목 래퍼의 min-width: 100% — 이 같은 컴포넌트를 슬라이더로 바꿉니다. 모든 카드가 화면을 채우고, 평범한 페이징 화살표가 정확히 한 항목씩 전진합니다.
1 / 6 · visible: trueTokyo
2 / 6 · visible: falseOslo
3 / 6 · visible: falseLima
4 / 6 · visible: falseCairo
5 / 6 · visible: falseSydney
6 / 6 · visible: falseQuito

화살표로 페이지 넘기기 — 각 슬라이드는 정확히 한 화면 너비이고, 각 슬라이드가 자신의 가시성을 보고합니다.

동작 방식

스토리는 .react-horizontal-scrolling-menu--item — 라이브러리가 각 자식 주위에 렌더링하는 div — 을 대상으로 하는 스타일 컨테이너로 메뉴를 감싸고, minWidth: ’100%’와 flex 가운데 정렬을 줍니다. 각 래퍼가 스크롤 컨테이너 전체에 걸치므로 카드 하나가 화면에 꼭 맞습니다. 화살표는 표준입니다. scrollPrev()scrollNext()는 가시 그룹만큼 페이지를 넘기고, 가시 그룹이 항목 하나일 때 페이지와 항목은 같은 것입니다.

화살표와 휠

화살표 상태는 useLeftArrowVisible()useRightArrowVisible()에서 옵니다 — 행이 그 끝에 있으면 각각 true를 반환하고, 스토리는 disabled에 넣고 버튼을 페이드아웃합니다. onWheel 프로퍼티는 이벤트와 함께 API 객체를 받으므로, 세로 마우스 휠이 deltaY의 부호로 행을 페이지 넘깁니다. 먼저 터치패드를 감지합니다. 수평 델타나 15 미만의 수직 델타는 터치패드 제스처로 보고 네이티브 스크롤에 남깁니다.

참고

전체 소스

완전하고 복사-붙여넣기 가능 — 이것이 바로 그 파일입니다. 출처는, 이 라이브 편집 가능한 Storybook 버전.

OneItem.source.tsx
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();
  }
}

관련 예제

예제 전체 21개