UNPKG

react-horizontal-scrolling-menu

Version:

Horizontal scrolling menu component for React with per-item visibility tracking. Supports mouse and touch devices, SSR-safe.

400 lines (320 loc) 23.8 kB
<!-- i18n:start --> [English](./README.md) · [简体中文](./README.zh-CN.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Русский](./README.ru.md) · Español · [Português (BR)](./README.pt-BR.md) · [Français](./README.fr.md) · [Tiếng Việt](./README.vi.md) <!-- i18n:meta locale=es source=README.md source-blob=8958730422d74e17cb64c668f1e52d7eeee19c63 status=translated --> <!-- i18n:end --> # React horizontal scrolling menu [![npm](https://img.shields.io/npm/v/react-horizontal-scrolling-menu.svg)](https://www.npmjs.com/package/react-horizontal-scrolling-menu) ![Descargas de npm](https://img.shields.io/npm/dm/react-horizontal-scrolling-menu) ![Tamaño del bundle npm (minified + gzip)](https://img.shields.io/bundlephobia/minzip/react-horizontal-scrolling-menu.svg) [![CI](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/actions/workflows/main.yml/badge.svg)](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/actions/workflows/main.yml) [![Disponible para contratar](https://img.shields.io/badge/available%20for%20hire-senior%20react%20engineer-2ea44f?style=flat-square)](https://asmyshlyaev177.dev) Un componente de menú con desplazamiento horizontal para React, construido sobre el desplazamiento nativo del navegador y con seguimiento de visibilidad por elemento. Ideal para filas de categorías, pestañas, filtros de chips, galerías: cualquier fila de elementos sobre la que tu aplicación necesite razonar. Los elementos son tus propios componentes con tu propio CSS; el menú se adapta al ancho de su contenedor; la navegación funciona con la barra de desplazamiento, el tacto, la rueda del ratón, el arrastre o los componentes de flecha que tú proporciones. 5,7 kB min+gzip. ![ejemplo](/sample.gif) ### [Página de inicio](https://react-horizontal-scrolling-menu.dev) · [Ejemplos en vivo (Storybook, editables en el navegador)](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu) · [API](#propiedades-y-callbacks) · [Habilidades para agentes de IA](#uso-con-agentes-de-ia) ### Quién lo usa Más de 20.000 repositorios dependen de esta biblioteca. Cinco que puedes leer — cada enlace lleva al `import` del componente que la usa, fijado a un commit, no a un `package.json`: - [Our World in Data](https://github.com/owid/owid-grapher/blob/4a60a2fb4532a2d287a1ef5660339dcc32bcd483/site/gdocs/components/KeyInsights.tsx#L3) — el carrusel de ideas clave en su renderizador de artículos; también sus [facetas de temas](https://github.com/owid/owid-grapher/blob/4a60a2fb4532a2d287a1ef5660339dcc32bcd483/site/latest/LatestTopicFacets.tsx#L10), que envuelven un `ToggleButton` de react-aria. `^8.2.0` - [Precious Plastic / ONE ARMY](https://github.com/ONEARMY/community-platform/blob/90c1be6be0ad450a92d9483577433fdc8b09f477/packages/components/src/VerticalList/VerticalList.client.tsx#L6-L7) — el `VerticalList` de su paquete de componentes compartidos, construido a partir de la propia documentación de esta biblioteca. `^8.2.0` - [erxes](https://github.com/erxes/erxes/blob/efef0252d390f4072e21c0a188d289f01866b188/apps/posclient-front/components/ui/horizontalScrollMenu.tsx#L6) — el menú de categorías en su cliente de punto de venta. `^4.0.4` - [Reapit](https://github.com/reapit/foundations/blob/9edda57691befd398547bcdf4013916b85face52/packages/app-builder/src/components/ui/viewport/tab-bar.tsx#L4) — la barra de pestañas del viewport en su constructor de aplicaciones. `^3.2.5` - [AWS Performance Dashboard](https://github.com/aws-solutions/performance-dashboard-on-aws/blob/cffa9c822ac8288a44d13a9394a2255e574c7592/frontend/src/components/Tabs.tsx#L8) — el componente `Tabs` del panel; sus [`Arrows`](https://github.com/aws-solutions/performance-dashboard-on-aws/blob/cffa9c822ac8288a44d13a9394a2255e574c7592/frontend/src/components/Arrows.tsx#L9) usan `VisibilityContext` directamente. Archivado en 2024, fija `^2.1.1`. También aparece en [React Status #257](https://react.statuscode.com/issues/257). ## Inicio rápido ```bash npm install react-horizontal-scrolling-menu ``` ```tsx import React from 'react'; import { ScrollMenu, VisibilityContext, type publicApiType, } from 'react-horizontal-scrolling-menu'; import 'react-horizontal-scrolling-menu/dist/styles.css'; const items = Array.from({ length: 10 }, (_, i) => `item-${i + 1}`); export function App() { return ( <ScrollMenu LeftArrow={LeftArrow} RightArrow={RightArrow}> {items.map((id) => ( <Card itemId={id} key={id} title={id} /> ))} </ScrollMenu> ); } function LeftArrow() { const visibility = React.useContext<publicApiType>(VisibilityContext); const isFirstVisible = visibility.useIsVisible('first', true); return ( <button disabled={isFirstVisible} onClick={() => visibility.scrollPrev()}> ← </button> ); } function RightArrow() { const visibility = React.useContext<publicApiType>(VisibilityContext); const isLastVisible = visibility.useIsVisible('last', false); return ( <button disabled={isLastVisible} onClick={() => visibility.scrollNext()}> → </button> ); } function Card({ itemId, title }: { itemId: string; title: string }) { const visibility = React.useContext<publicApiType>(VisibilityContext); const isVisible = visibility.useIsVisible(itemId); return ( <div style={{ width: '160px' }} data-visible={isVisible}> {title} </div> ); } ``` Tres cosas de las que depende el ejemplo: - Cada elemento necesita una prop `itemId` única: así funciona el seguimiento de visibilidad. La `key` de React funciona como respaldo. - `styles.css` es una importación aparte; el bundle JS nunca inyecta CSS. - El ancho del elemento viene de tu propio CSS: el menú no mide nada. ¿Escribes JavaScript puro? Quita las importaciones de tipos y usa `React.useContext(VisibilityContext)` como siempre. ## Uso con agentes de IA Los modelos entrenados con versiones antiguas siguen buscando `visibleElements`, elementos `Separator` y una prop `Arrows` — todo eliminado — e inventan una prop `autoplay` que nunca existió. El paquete incluye ocho archivos `SKILL.md` para evitarlo: guías por tareas que se cargan bajo demanda a través de [TanStack Intent](https://tanstack.com/intent/latest/docs/overview), versionadas con la biblioteca y no con ninguna página web. ```bash npm install react-horizontal-scrolling-menu npx @tanstack/intent@latest install # una vez por proyecto ``` `install` añade el descubrimiento de habilidades a la configuración de tu agente (`CLAUDE.md`, `.cursorrules`, …); el agente carga entonces una habilidad bajo demanda desde `node_modules/react-horizontal-scrolling-menu/skills/`. Lista o cárgalas directamente con `npx @tanstack/intent@latest list` y `npx @tanstack/intent@latest load react-horizontal-scrolling-menu#menu-setup`. | Habilidad | Cuándo se carga | | ---------------------- | ---------------------------------------------------------------------- | | `menu-setup` | Un primer menú funcional, flechas, la importación CSS requerida | | `menu-visibility` | Qué hay en pantalla y el estado de las flechas en los extremos | | `menu-scrolling` | `scrollToItem`, `apiRef`, paginación página a página | | `menu-interactions` | Arrastre, rueda y tacto — y sus fábricas de manejadores | | `menu-recipes` | Autoplay, bucle infinito, cargar más: recetas, no props | | `menu-transitions-rtl` | Tiempo de animación, easing personalizado, derecha a izquierda | | `menu-testing-ssr` | Next.js y RSC, mocks de Jest, Playwright | | `menu-migration` | Actualizar código anterior a v8 y las API que los modelos aún inventan | El código fuente vive en [`skills/`](skills/). Los agentes que no pueden cargar habilidades de Intent deberían leer [llms.txt](https://react-horizontal-scrolling-menu.dev/llms.txt): los mismos hechos, condensados en un archivo. ## Qué hace — y qué no Construido sobre el desplazamiento nativo del navegador: la inercia, la barra de desplazamiento, el tacto, la rueda y la accesibilidad vienen del navegador, no de una reimplementación de física. Encima de eso: visibilidad por elemento mediante IntersectionObserver, `scrollToItem` / `scrollNext` / `scrollPrev`, un `apiRef` para el control desde fuera, ranuras Header y Footer, RTL, detección dinámica de añadido/eliminación y tipos de TypeScript por todas partes. Seguro para SSR: la [página de inicio](https://react-horizontal-scrolling-menu.dev) renderiza cada demo en el servidor. Sin motor de carrusel: sin física de ajuste ni de muelle — si quieres un slider de imágenes a pantalla completa, usa Embla o Swiper. El autoplay y el bucle infinito tampoco son props; son recetas de unas sesenta líneas cada una sobre la API pública, editables en vivo en Storybook ([bucle infinito](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-infiniteloop--infinite-loop), [autoplay](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-autoplay--autoplay)). Si necesitas una fila que sepa qué es visible, esto es para ti. ## Ejemplos Cada ejemplo es editable en vivo en [Storybook](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu): cada historia viene con un editor Monaco cargado con las definiciones de tipo reales de la biblioteca. Cubre: uso básico, desplazamiento de un elemento por vez, arrastre con el ratón, desplazarse a un elemento al montar, centrar al hacer clic, añadir elementos dinámicamente, guardar/restaurar posición, animación de elementos, puntos de progreso, evitar el desplazamiento del body, transiciones personalizadas, bucle infinito, autoplay, diseño vertical, flechas en el pie, deslizamiento móvil, RTL y una prueba de estrés con 5.000 elementos. <!-- DOCS_START --> ### Helpers y API Los hijos del componente principal ScrollMenu (flechas, cabecera, pie, elementos) pueden usar **VisibilityContext** para acceder al estado y los callbacks. Los callbacks de función también reciben el contexto, por ejemplo `onWheel`, `onScroll`. ## Propiedades y callbacks | Prop | Firma | | ------------------------ | ----------------------------------------------------------------------------------------------------------- | | LeftArrow | Componente React para la flecha izquierda | | RightArrow | Componente React para la flecha derecha | | Header | Componente React Header | | Footer | Componente React Footer | | onWheel | (VisibilityContext, event) => void | | onScroll | (VisibilityContext, event) => void, se dispara _antes_ de que el desplazamiento se asiente | | onInit | (VisibilityContext) => void | | onUpdate | (VisibilityContext) => void | | apiRef | React.RefObject \| React.RefCallback | | options | opciones para IntersectionObserver: `rootMargin`, `threshold` y `ratio` para considerar un elemento visible | | containerRef | React.RefObject \| React.RefCallback para el contenedor de desplazamiento | | onMouseDown | (VisibilityContext) => (React.MouseEventHandler) => void | | onMouseLeave | (VisibilityContext) => (React.MouseEventHandler) => void | | onMouseUp | (VisibilityContext) => (React.MouseEventHandler) => void | | onMouseMove | (VisibilityContext) => (React.MouseEventHandler) => void | | onTouchMove | (VisibilityContext) => (React.TouchEventHandler) => void | | onTouchStart | (VisibilityContext) => (React.TouchEventHandler) => void | | onTouchEnd | (VisibilityContext) => (React.TouchEventHandler) => void | | itemClassName | ClassName del Item | | scrollContainerClassName | ClassName del scrollContainer | | wrapperClassName | ClassName del div más externo | | transitionDuration | Duración de las transiciones en ms, por defecto `500`, requiere `noPolyfill={false}` | | transitionBehavior | 'smooth' \| 'auto' \| función personalizada, requiere `noPolyfill={false}` | | RTL | Activar la dirección de derecha a izquierda | | noPolyfill | `true` por defecto (scrollIntoView nativo); pon `false` para activar las props de transición | Observa las dos formas de callback: `onWheel` y `onScroll` son simples `(context, event) => void`, mientras que las props de ratón y táctiles son fábricas de manejadores — `(context) => (event) => void`. Consulta la [historia MouseDrag](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-mousedrag--mouse-drag) para ver el patrón de fábrica en uso. ### VisibilityContext Hooks (llámalos solo dentro de componentes renderizados bajo ScrollMenu, siguiendo las reglas de los hooks): | Hook | Firma | | -------------------- | ------------------------------------------------------------------------ | | useIsVisible | (itemId: string \| 'first' \| 'last', defaultValue?: boolean) => boolean | | useLeftArrowVisible | () => boolean | | useRightArrowVisible | () => boolean | Valores y funciones: | Prop | Firma | | --------------------- | ------------------------------------------------------ | | getItemById | itemId => IOItem \| undefined | | getItemElementById | itemId => DOM Element \| null | | getItemByIndex | index => IOItem \| undefined | | getItemElementByIndex | index => DOM Element \| null | | getNextElement | () => IOItem \| undefined | | getPrevElement | () => IOItem \| undefined | | isFirstItemVisible | boolean | | isItemVisible | itemId => boolean | | isLastItem | boolean | | isLastItemVisible | boolean | | menuVisible | { current: boolean } | | scrollNext | (behavior, inline, block, ScrollOptions) => void | | scrollPrev | (behavior, inline, block, ScrollOptions) => void | | scrollToItem | (item, behavior, inline, block, ScrollOptions) => void | | items | instancia de la clase ItemsMap | | scrollContainer | Ref<OuterContainer> | ### instancia de la clase items ItemsMap guarda información sobre todos los elementos, con métodos para obtener los elementos actualmente visibles y el elemento anterior o siguiente. También puedes suscribirte a actualizaciones. | Prop/método | Descripción | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | subscribe | suscribirse a eventos para `itemId` o `first`, `last`, `onInit`, `onUpdate`, p. ej. `items.subscribe('item5', (item) => setVisible(item.visible))` | | unsubscribe | úsalo en useEffect para la limpieza, pasa la misma instancia de callback | | getVisible | devuelve solo los elementos visibles | | toItems | devuelve los id de todos los elementos | | toArr | devuelve todos los elementos | | first | devuelve el primer elemento | | last | devuelve el último elemento | | prev | (itemId \| Item) => elemento anterior \| undefined | | next | (itemId \| Item) => elemento siguiente \| undefined | ### Transiciones y animación `transitionDuration` y `transitionBehavior` (`'smooth'`, `'auto'` o una función personalizada) controlan cómo animan `scrollToItem` y los helpers de desplazamiento. Ambos requieren `noPolyfill={false}`: el desplazamiento nativo por defecto los ignora. No se combinan con la prop `RTL`. Consulta la [historia CustomTransition](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-customtransition--custom-transition) para una función de easing personalizada. #### ScrollOptions El último argumento de `scrollToItem`, `scrollPrev` y `scrollNext` anula las props de transición para esa única llamada: ```tsx scrollToItem(getItemElementById('item-5'), 'smooth', 'center', 'nearest', { duration: 800, // milisegundos }); ``` ### Otros helpers #### slidingWindow Obtén el grupo anterior o siguiente de elementos visibles: ```tsx slidingWindow(allItems, visibleItems).prev(); // o .next() ``` #### getItemsPos Obtén el primer elemento, el central y el último de un grupo; por ejemplo, para desplazarte al centro de la página anterior: ```tsx const prevGroup = slidingWindow(allItems, visibleItems).prev(); const { center } = getItemsPos(prevGroup); scrollToItem(getItemById(center), 'smooth', 'center'); ``` ### apiRef Pasa una ref a ScrollMenu y se le asigna el valor completo de VisibilityContext — útil para disparar funciones como `scrollToItem` desde fuera del menú. Los valores de datos en la ref pueden quedar obsoletos, así que prefiere llamar a funciones: ```tsx apiRef.current.scrollToItem(apiRef.current.getItemElementById('item-3')); ``` También puedes acceder directamente al elemento DOM de un elemento mediante ``document.querySelector(`[data-key='${itemId}']`)``. Consulta la [historia ScrollToItem](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-scrolltoitem--scroll-to-item) y la [historia AddItemAndScrollToIt](https://asmyshlyaev177.github.io/react-horizontal-scrolling-menu/?path=/story/examples-additemandscrolltoit--add-item-and-scroll-to-it). <!-- DOCS_END --> ## SSR La biblioteca es segura para SSR: el primer render emite marcado plano e IntersectionObserver solo se conecta en el cliente. El argumento `defaultValue` de `useIsVisible` controla el estado renderizado en el servidor: el patrón canónico de flechas (`('first', true)` / `('last', false)`) renderiza una flecha izquierda deshabilitada y una derecha habilitada, lo que corresponde a una fila desplazada a su inicio. ### Nota sobre Next.js El paquete es ESM-first. En configuraciones antiguas de Next.js puedes toparte con [“Cannot use import statement outside a module”](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/issues/240): añadir el paquete a [`transpilePackages`](https://nextjs.org/docs/app/api-reference/config/next-config-js/transpilePackages) lo resuelve. ## Soporte de navegadores Requiere **IntersectionObserver** y **requestAnimationFrame**: todo navegador moderno. Sin IE. ## Desarrollo ```bash git clone https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu cd react-horizontal-scrolling-menu pnpm run setup pnpm run demo # app de ejemplo (Next.js, puerto 3003) con la biblioteca en modo watch pnpm run demo-tanstack # app de ejemplo (TanStack Start SSR, puerto 3004) pnpm run storybook # ejemplos pnpm test # pruebas de unidad + e2e + storybook ``` En el repositorio viven dos apps de ejemplo de integración — `example-nextjs` y `example-tanstack` (TanStack Start, renderizado en servidor en workerd) — ambas renderizan la misma demo (arrastre con ratón, bloqueo del desplazamiento del body, animación personalizada con un panel de control) para que la única suite e2e en `e2e/` se ejecute contra la biblioteca en ambos frameworks, incluida una comprobación de que el menú ya está presente en el HTML renderizado en el servidor. Las contribuciones y correcciones son bienvenidas: haz fork, haz commit, abre un PR y no olvides las pruebas. Consulta [CONTRIBUTING](./CONTRIBUTING.md) y [CHANGELOG](./CHANGELOG.md). Documentación de la [API v1](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/tree/v1) heredada. ## Acerca de Construido y mantenido por **Aleksandr Smyshliaev** desde 2018: mi primer paquete de npm y la misma API pública de React 16.8 a 19. Soy ingeniero frontend (React / Next.js / TypeScript) y **estoy disponible para trabajo por contrato y a tiempo completo**. - **Contacta conmigo** — [asmyshlyaev177.dev](https://asmyshlyaev177.dev) · [asmyshlyaev177@gmail.com](mailto:asmyshlyaev177@gmail.com) · [LinkedIn](https://linkedin.com/in/asmyshlyaev177) · Telegram @asmyshlyaev177 - **También míos** — [state-in-url](https://github.com/asmyshlyaev177/state-in-url) (estado tipado en la URL), [test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder) (grabar/reproducir para Playwright) Una ⭐️ en el repositorio ayuda a que más gente encuentre la biblioteca.