├── .changeset ├── README.md └── config.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1.bug.md │ └── config.yml ├── dependabot.yml └── workflows │ ├── main.yml │ └── virtuoso.dev.yml ├── .gitignore ├── .node-version ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── apps └── virtuoso.dev │ ├── .gitignore │ ├── .sample.env.local │ ├── babel.config.js │ ├── docs │ ├── .nojekyll │ ├── guides │ │ ├── getting-started.md │ │ ├── grouped-virtuoso │ │ │ ├── _category_.yml │ │ │ ├── grouped-by-first-letter.md │ │ │ ├── grouped-numbers.md │ │ │ ├── grouped-with-load-on-demand.md │ │ │ └── scroll-to-group.md │ │ ├── masonry │ │ │ ├── _category_.yml │ │ │ └── example.md │ │ ├── migrate-v0-to-v1.md │ │ ├── table-virtuoso │ │ │ ├── _category_.yml │ │ │ ├── hello-table.md │ │ │ ├── table-fixed-columns.md │ │ │ └── table-fixed-headers.md │ │ ├── third-party-integration │ │ │ ├── _category_.yml │ │ │ ├── material-ui-endless-scrolling.md │ │ │ ├── mocking-in-tests.md │ │ │ ├── mui-table-virtual-scroll.md │ │ │ └── tanstack-table-integration.md │ │ ├── troubleshooting.md │ │ ├── virtuoso-grid │ │ │ ├── _category_.yml │ │ │ └── grid-responsive-columns.md │ │ ├── virtuoso-message-list │ │ │ ├── _category_.yml │ │ │ ├── context.md │ │ │ ├── custom-scrollbar.md │ │ │ ├── examples │ │ │ │ ├── 01-messaging.md │ │ │ │ ├── 02-ai-chatbot.md │ │ │ │ ├── 03-gemini.md │ │ │ │ ├── 04-reactions.md │ │ │ │ ├── 05-date-separators.md │ │ │ │ ├── 06-scroll-to-reply.md │ │ │ │ ├── 07-grouped-messages.md │ │ │ │ └── _category_.yml │ │ │ ├── headers-footers.md │ │ │ ├── hooks.md │ │ │ ├── item-keys.md │ │ │ ├── licensing.md │ │ │ ├── overview.md │ │ │ ├── resize-observer-errors.md │ │ │ ├── scrolling-to-item.md │ │ │ ├── smooth-scrolling.md │ │ │ ├── testing.md │ │ │ ├── tutorial │ │ │ │ ├── 01-intro.md │ │ │ │ ├── 02-message-list.md │ │ │ │ ├── 03-loading-older-messages.md │ │ │ │ ├── 04-scroll-to-bottom-button.md │ │ │ │ ├── 05-receive-messages.md │ │ │ │ ├── 06-send-messages.md │ │ │ │ ├── 07-multiple-channels.md │ │ │ │ └── _category_.yml │ │ │ └── working-with-data.md │ │ └── virtuoso │ │ │ ├── _category_.yml │ │ │ ├── auto-resizing.md │ │ │ ├── custom-scroll-container.md │ │ │ ├── customize-structure.md │ │ │ ├── endless-scrolling.md │ │ │ ├── footer.md │ │ │ ├── hello.md │ │ │ ├── horizontal-mode.md │ │ │ ├── initial-index.md │ │ │ ├── keyboard-navigation.md │ │ │ ├── press-to-load-more.md │ │ │ ├── range-change-callback.md │ │ │ ├── scroll-handling.md │ │ │ ├── scroll-seek-placeholders.md │ │ │ ├── scroll-to-index.md │ │ │ ├── top-items.md │ │ │ └── window-scrolling.md │ └── virtuoso-masonry-api │ │ └── variables │ │ └── VirtuosoMasonry.md │ ├── docusaurus.config.ts │ ├── eslint.config.mjs │ ├── package.json │ ├── sidebars.ts │ ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ ├── pages │ │ ├── message-list-eula.md │ │ ├── pricing.tsx │ │ ├── privacy-policy.md │ │ └── terms-of-use.md │ └── theme │ │ ├── CodeBlock │ │ ├── Container │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Content │ │ │ ├── Element.tsx │ │ │ ├── String.tsx │ │ │ └── styles.module.css │ │ ├── CopyButton │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Line │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── LiveCodeBlock │ │ │ ├── createCodesandbox.ts │ │ │ ├── esmTransform.ts │ │ │ ├── extraImports.ts │ │ │ ├── iframe-style.css │ │ │ └── index.tsx │ │ ├── WordWrapButton │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── index.tsx │ │ ├── ColorModeToggle │ │ ├── index.tsx │ │ └── styles.module.css │ │ ├── Icon │ │ └── ExternalLink │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Layout │ │ └── Provider │ │ │ └── index.tsx │ │ └── Navbar │ │ └── Logo │ │ ├── index.tsx │ │ └── logo.svg │ ├── static │ ├── .nojekyll │ ├── CNAME │ └── img │ │ ├── avatar.png │ │ ├── favicon.ico │ │ ├── full-logo.png │ │ ├── logo.svg │ │ ├── navbar-bg.png │ │ └── social-card.png │ ├── tsconfig.json │ ├── tsconfig.masonry.json │ └── tsconfig.message-list.json ├── package-lock.json ├── package.json ├── packages ├── gurx │ ├── .gitignore │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── prettier.config.mjs │ ├── src │ │ ├── AsyncQuery.ts │ │ ├── RefCount.ts │ │ ├── SetMap.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── operators.ts │ │ ├── react.tsx │ │ ├── realm.ts │ │ ├── test │ │ │ ├── browser │ │ │ │ ├── __screenshots__ │ │ │ │ │ └── react.test.tsx │ │ │ │ │ │ └── Gurx-React-Realm-renders-a-cell-value-1.png │ │ │ │ ├── react.test.tsx │ │ │ │ └── renderHook.tsx │ │ │ └── node │ │ │ │ ├── AsyncQuery.test.ts │ │ │ │ ├── RefCount.test.ts │ │ │ │ ├── pipe.test.ts │ │ │ │ └── realm.test.ts │ │ ├── utils.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.workspace.ts ├── masonry │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── prettier.config.mjs │ ├── src │ │ ├── VirtuosoMasonry.tsx │ │ ├── _stories │ │ │ └── masonry.stories.tsx │ │ ├── content.tsx │ │ ├── data.ts │ │ ├── dom.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── masonry-item-state.ts │ │ ├── masonry-sizes.ts │ │ ├── sizing │ │ │ ├── AATree.test.ts │ │ │ ├── AATree.ts │ │ │ ├── binaryArraySearch.ts │ │ │ ├── deleteIndices.test.ts │ │ │ ├── deleteIndices.ts │ │ │ ├── insertRanges.ts │ │ │ ├── offsetTreeReducer.ts │ │ │ ├── rangesWithinOffsets.ts │ │ │ └── sizeTreeReducer.ts │ │ ├── test │ │ │ └── node │ │ │ │ └── ssr.test.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.workspace.ts ├── react-virtuoso │ ├── .ladle │ │ └── config.mjs │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── e2e │ │ ├── collapsible-long-item.test.ts │ │ ├── data.test.ts │ │ ├── follow-output-loading-image.test.ts │ │ ├── gap.test.ts │ │ ├── grid-gap.test.ts │ │ ├── grid-scroll-seek-placeholder.test.ts │ │ ├── grid.test.ts │ │ ├── grouped-topmost-item.test.ts │ │ ├── grouped.test.ts │ │ ├── hello.test.ts │ │ ├── initial-scroll-top.test.ts │ │ ├── initial-topmost-item.test.ts │ │ ├── long-last-item.test.ts │ │ ├── prepend-items.test.ts │ │ ├── reverse-taller-than-viewport.test.ts │ │ ├── scroll-seek-placeholder.test.ts │ │ ├── scroll-to-index.test.ts │ │ ├── table.test.ts │ │ ├── test-case-446.test.ts │ │ ├── toggle.test.ts │ │ ├── top-items.test.ts │ │ └── utils.ts │ ├── eslint.config.mjs │ ├── examples │ │ ├── align-to-bottom.tsx │ │ ├── at-bottom-detection.tsx │ │ ├── auto-prepend-items.tsx │ │ ├── benchmark.tsx │ │ ├── collapsible-long-item.tsx │ │ ├── context.tsx │ │ ├── custom-components.tsx │ │ ├── custom-scroller.tsx │ │ ├── data.tsx │ │ ├── decrease-items.tsx │ │ ├── default-item-height.tsx │ │ ├── empty-placeholder.tsx │ │ ├── end-reached.tsx │ │ ├── endless-scrolling.tsx │ │ ├── expandable-cards.tsx │ │ ├── follow-output-async-expanded.tsx │ │ ├── follow-output-loading-image.tsx │ │ ├── follow-output.tsx │ │ ├── gap.tsx │ │ ├── grid-data.tsx │ │ ├── grid-gap.tsx │ │ ├── grid-header-footer.tsx │ │ ├── grid-infinite-data.tsx │ │ ├── grid-initial-item-count-initial-topmost-item-index.tsx │ │ ├── grid-initial-item-count.tsx │ │ ├── grid-initial-topmost-item-index.tsx │ │ ├── grid-optimize-rendering.tsx │ │ ├── grid-responsive-columns.tsx │ │ ├── grid-scroll-seek-placeholder.tsx │ │ ├── grid.tsx │ │ ├── group-scroll-into-view.tsx │ │ ├── group-scroll-seek-placeholder.tsx │ │ ├── grouped-endless-scrolling.tsx │ │ ├── grouped-load-more.tsx │ │ ├── grouped-prepend-items.tsx │ │ ├── grouped-topmost-item.tsx │ │ ├── grouped.tsx │ │ ├── header-footer.tsx │ │ ├── hello.tsx │ │ ├── horizontal.tsx │ │ ├── iframe-portal.tsx │ │ ├── initial-from-zero.tsx │ │ ├── initial-scroll-top.tsx │ │ ├── initial-topmost-item-default-height.tsx │ │ ├── initial-topmost-item.tsx │ │ ├── iti-multiple.tsx │ │ ├── long-last-item.tsx │ │ ├── prepend-as-you-scroll.tsx │ │ ├── prepend-items.tsx │ │ ├── prepend-last-tall-item.tsx │ │ ├── react-beautiful-dnd-window-scroller.tsx │ │ ├── react-beautiful-dnd.tsx │ │ ├── remove-items.tsx │ │ ├── rerender.tsx │ │ ├── resize-loop.tsx │ │ ├── resizing-items.tsx │ │ ├── reverse-taller-than-viewport.tsx │ │ ├── scroll-element-grid-scroller.tsx │ │ ├── scroll-element-list-scroller.tsx │ │ ├── scroll-into-view-align.tsx │ │ ├── scroll-into-view.tsx │ │ ├── scroll-seek-placeholder.tsx │ │ ├── scroll-to-index.tsx │ │ ├── ssr.tsx │ │ ├── start-reached.tsx │ │ ├── state.tsx │ │ ├── subtitles-ui.tsx │ │ ├── table-markup.tsx │ │ ├── table-state.tsx │ │ ├── tall-item-reverse-scrolling.tsx │ │ ├── tanstack-table-flexlayout.tsx │ │ ├── test-case-1234.tsx │ │ ├── test-case-446.tsx │ │ ├── test-case-460.tsx │ │ ├── test-case-463.tsx │ │ ├── test-case-638.tsx │ │ ├── test-case-722.tsx │ │ ├── test-case-896.tsx │ │ ├── toggle-display-grouped.tsx │ │ ├── toggle.tsx │ │ ├── top-items.tsx │ │ ├── upward-scroll.tsx │ │ ├── window-grid.tsx │ │ ├── window-group-scroll.tsx │ │ ├── window-scroller.tsx │ │ └── window-table.tsx │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ ├── AATree.ts │ │ ├── TableVirtuoso.tsx │ │ ├── Virtuoso.tsx │ │ ├── VirtuosoGrid.tsx │ │ ├── alignToBottomSystem.ts │ │ ├── comparators.tsx │ │ ├── component-interfaces │ │ │ ├── TableVirtuoso.ts │ │ │ ├── Virtuoso.ts │ │ │ └── VirtuosoGrid.ts │ │ ├── domIOSystem.ts │ │ ├── followOutputSystem.ts │ │ ├── gridSystem.ts │ │ ├── groupedListSystem.ts │ │ ├── hooks │ │ │ ├── __mocks__ │ │ │ │ ├── useChangedChildSizes.ts │ │ │ │ ├── useScrollTop.ts │ │ │ │ └── useSize.ts │ │ │ ├── useChangedChildSizes.ts │ │ │ ├── useIsomorphicLayoutEffect.ts │ │ │ ├── useScrollTop.ts │ │ │ ├── useSize.ts │ │ │ └── useWindowViewportRect.ts │ │ ├── index.tsx │ │ ├── initialItemCountSystem.ts │ │ ├── initialScrollTopSystem.ts │ │ ├── initialTopMostItemIndexSystem.ts │ │ ├── interfaces.ts │ │ ├── listStateSystem.ts │ │ ├── listSystem.ts │ │ ├── loggerSystem.ts │ │ ├── propsReadySystem.ts │ │ ├── react-urx │ │ │ └── index.tsx │ │ ├── recalcSystem.ts │ │ ├── scrollIntoViewSystem.ts │ │ ├── scrollSeekSystem.ts │ │ ├── scrollToIndexSystem.ts │ │ ├── sizeRangeSystem.ts │ │ ├── sizeSystem.ts │ │ ├── stateFlagsSystem.ts │ │ ├── stateLoadSystem.ts │ │ ├── topItemCountSystem.ts │ │ ├── totalListHeightSystem.ts │ │ ├── upwardScrollFixSystem.ts │ │ ├── urx │ │ │ ├── actions.ts │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── pipe.ts │ │ │ ├── streams.ts │ │ │ ├── system.ts │ │ │ ├── transformers.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── approximatelyEqual.ts │ │ │ ├── binaryArraySearch.ts │ │ │ ├── context.ts │ │ │ ├── correctItemSize.ts │ │ │ ├── positionStickyCssValue.ts │ │ │ ├── simpleMemoize.ts │ │ │ └── skipFrames.ts │ │ └── windowScrollerSystem.ts │ ├── test │ │ ├── AATree.test.ts │ │ ├── Grid.test.tsx │ │ ├── Virtuoso.test.tsx │ │ ├── VirtuosoMockContext.test.tsx │ │ ├── __snapshots__ │ │ │ └── VirtuosoMockContext.test.tsx.snap │ │ ├── binaryArraySearch.test.ts │ │ ├── gridSystem.test.ts │ │ ├── groupedListSystem.test.ts │ │ ├── listSystem.test.ts │ │ ├── react-urx │ │ │ ├── index.test.tsx │ │ │ └── ssr.test.tsx │ │ ├── sizeRangeSystem.test.ts │ │ ├── sizeSystem.test.ts │ │ ├── ssr.test.tsx │ │ ├── tsconfig.json │ │ ├── urx │ │ │ ├── actions.test.ts │ │ │ ├── combineLatest.test.ts │ │ │ ├── eventHandler.test.ts │ │ │ ├── merge.test.ts │ │ │ ├── operators.test.ts │ │ │ ├── statefulStream.test.ts │ │ │ ├── stream.test.ts │ │ │ ├── system.test.ts │ │ │ └── utils.test.ts │ │ └── windowScrollerSystem.test.ts │ ├── tsconfig.json │ └── vite.config.ts └── tooling │ ├── README.md │ ├── eslint.config.d.mts │ ├── eslint.config.mjs │ ├── package.json │ ├── prettier.config.d.mts │ ├── prettier.config.mjs │ ├── tsconfig.base.json │ └── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: petyosi 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something went wrong? Please provide a reproduction! Please search for similar issues first to make sure it is not already covered. 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 11 | 12 | **Describe the bug** 13 | 14 | A clear and concise description of what the bug is. 15 | 16 | **Reproduction** 17 | 18 | Use https://codesandbox.io/ to illustrate the problem so that I can observe the issue on my side and make sure that a potential fix reliably addresses it. 19 | 20 | **To Reproduce** 21 | 22 | Steps to reproduce the behavior: 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Expected behavior** 29 | 30 | A clear and concise description of what you expected to happen. 31 | 32 | **Screenshots** 33 | 34 | If applicable, add screenshots to help explain your problem. 35 | 36 | **Desktop (please complete the following information):** 37 | 38 | - OS: [e.g. iOS] - Windows can behave unexpectedly. 39 | - Browser [e.g. chrome, safari] 40 | 41 | **Additional context** 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true # force the usage of a template 2 | contact_links: 3 | - name: Feature Requests, support and consulting 4 | url: https://github.com/sponsors/petyosi 5 | about: If you have troubles integrating React Virtuoso in your project and need dedicated assistance with it, contact me over my the email in my profile. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Use `allow` to specify which dependencies to maintain 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "npm" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | allow: 10 | - dependency-type: "production" 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | env: 8 | CI: true 9 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 10 | USE_SSH: true 11 | GIT_USER: petyosi 12 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 13 | 14 | steps: 15 | - name: Begin CI... 16 | uses: actions/checkout@v4 17 | 18 | - name: Use Node 22 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 22.x 22 | 23 | - name: Install dependencies 24 | run: | 25 | npm install 26 | npm install --workspaces 27 | 28 | - name: run CI checks 29 | run: | 30 | npm run ci 31 | 32 | - name: Release 33 | id: changesets 34 | if: github.ref == 'refs/heads/master' 35 | uses: changesets/action@v1 36 | with: 37 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 38 | publish: npm run release 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | 43 | - uses: actions/upload-artifact@v4 44 | if: ${{ !cancelled() }} 45 | with: 46 | name: playwright-report-react-virtuoso 47 | path: packages/react-virtuoso/playwright-report/ 48 | retention-days: 7 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | build 7 | .turbo 8 | test-results 9 | 10 | # generated docs 11 | docs/virtuoso-api/* 12 | docs/virtuoso-message-list-api/* 13 | docs/virtuoso-masonry-api/* 14 | apps/virtuoso.dev/docs/virtuoso-masonry-api/* 15 | .vscode 16 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v22.13.1 2 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # React Virtuoso Codebase Guide 2 | 3 | ## Build Commands 4 | - Build: `npm run build` or `turbo build` 5 | - Lint: `npm run lint` 6 | - Test: `npm run test` 7 | - Run single test: `npx vitest ` or `npx vitest -t ""` 8 | - E2E tests: `npm run e2e` or `npx playwright test ` 9 | - Dev environment: `npm run dev` 10 | 11 | ## Code Style Guidelines 12 | - Use TypeScript with strong typing; avoid `any` when possible 13 | - Format: prettier with 140 char width, single quotes, no semicolons 14 | - Naming: camelCase for variables/functions, PascalCase for components/classes 15 | - Imports: group React imports first, then external libs, then internal modules 16 | - Prefer functional components with hooks over class components 17 | - Use the urx state management system for component state 18 | - Error handling: prefer early returns over deep nesting 19 | - Use test-driven development with vitest for unit tests 20 | - Keep components focused on a single responsibility 21 | 22 | ## Performance Considerations 23 | - Optimize rendering with memoization where appropriate 24 | - Ensure proper cleanup in useEffect hooks 25 | - Be cautious with closures capturing outdated values -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to React Virtuoso 2 | 3 | ## Run the project locally 4 | 5 | The repository uses NPM, [turborepo](https://turbo.build/), and [changesets](https://github.com/changesets/changesets) for its infrastructure. The `react-virtuoso` package has a [ladle](https://ladle.dev/) setup for examples, test cases, and bug reproductions. Run `npm run dev` inside `packages/react-virtuoso`. 6 | 7 | ## Add fixes and new features 8 | 9 | Virtuoso has an extensive unit/E2E test suite. To run the unit tests, use `npm run test`. You can run the end-to-end browser-based test suite with `npm run e2e`, which executes a Playwright test suite against the ladle examples. 10 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | ./node_modules 3 | 4 | # Production 5 | /build 6 | docs/virtuoso-api/* 7 | docs/virtuoso-message-list-api/* 8 | 9 | # Generated files 10 | .docusaurus 11 | .cache-loader 12 | 13 | # Misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/.sample.env.local: -------------------------------------------------------------------------------- 1 | PADDLE_ENVIRONMENT='sandbox' 2 | PADDLE_TOKEN=test_5 3 | PADDLE_STANDARD_PRICE_ID=pri_01 4 | PADDLE_PRO_PRICE_ID=pri_01 5 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | } 4 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/grouped-virtuoso/_category_.yml: -------------------------------------------------------------------------------- 1 | label: Grouped Virtuoso 2 | position: 4 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/grouped-virtuoso/grouped-numbers.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: grouped-numbers 3 | title: Grouped 10,000 numbers 4 | sidebar_label: Grouped Numbers 5 | slug: /grouped-numbers/ 6 | sidebar_position: 1 7 | --- 8 | 9 | The example below shows a simple grouping mode - 10,000 items in groups of 10. 10 | 11 | ```tsx live 12 | import { GroupedVirtuoso } from 'react-virtuoso' 13 | import { useMemo } from 'react' 14 | 15 | export default function App() { 16 | const groupCounts = useMemo(() => { 17 | return Array(1000).fill(10) 18 | }, []) 19 | 20 | return ( 21 | { 25 | return ( 26 |
Group {index * 10} - {index * 10 + 10}
27 | ) 28 | }} 29 | itemContent={(index, groupIndex) => (
{index} (group {groupIndex})
) } 30 | /> 31 | ) 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/masonry/_category_.yml: -------------------------------------------------------------------------------- 1 | label: Masonry 2 | position: 8 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/table-virtuoso/_category_.yml: -------------------------------------------------------------------------------- 1 | label: Table Virtuoso 2 | position: 5 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/table-virtuoso/table-fixed-columns.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: table-fixed-columns 3 | title: Table Virtuoso Example with Fixed Columns 4 | sidebar_label: Fixed Columns 5 | slug: /table-fixed-columns/ 6 | --- 7 | 8 | Setting sticky columns is done entirely through styling. 9 | 10 | ## Table with fixed first column 11 | 12 | ```tsx live 13 | import {TableVirtuoso} from 'react-virtuoso' 14 | import {useMemo} from 'react' 15 | 16 | export default function App() { 17 | const users = useMemo(() => { 18 | return Array.from({ length: 1000 }, (_, index) => ({ 19 | name: `User ${index}`, 20 | description: `Description for user ${index}` 21 | })) 22 | }, []) 23 | 24 | return ( 25 | }} 29 | fixedHeaderContent={() => ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | )} 39 | itemContent={(index, user) => ( 40 | <> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | )} 49 | /> 50 | ) 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/table-virtuoso/table-fixed-headers.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: table-fixed-headers 3 | title: Table Virtuoso Example with Fixed Headers 4 | sidebar_label: Fixed Headers 5 | slug: /table-fixed-headers/ 6 | --- 7 | 8 | If set, the `fixedHeaderContent` property specifies the content of the `thead` element. The header element remains fixed while scrolling. 9 | Ensure that the header elements are not transparent. Otherwise, the table cells will be visible. 10 | 11 | ## Table with `fixedHeaderContent` 12 | 13 | ```tsx live 14 | import {TableVirtuoso} from 'react-virtuoso' 15 | import {useMemo} from 'react' 16 | 17 | export default function App() { 18 | const users = useMemo(() => { 19 | return Array.from({ length: 1000 }, (_, index) => ({ 20 | name: `User ${index}`, 21 | description: `Description for user ${index}` 22 | })) 23 | }, []) 24 | 25 | return ( 26 | ( 30 | 31 | 32 | 33 | 34 | )} 35 | itemContent={(index, user) => ( 36 | <> 37 | 38 | 39 | 40 | )} 41 | /> 42 | ) 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/third-party-integration/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Third-Party Integration" 2 | position: 6 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-grid/_category_.yml: -------------------------------------------------------------------------------- 1 | label: Virtuoso Grid 2 | position: 7 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Virtuoso Message List" 2 | position: 2 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/context.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: virtuoso-message-list-context 3 | title: Context 4 | sidebar_label: Context 5 | sidebar_position: 6 6 | slug: /virtuoso-message-list/context 7 | --- 8 | 9 | # Context 10 | 11 | In addition to the `data` prop, the message list component accepts a `context` prop, which can be used to pass additional data updates to the `ItemContent` and the custom headers and footers, if present. The `context` prop can be anything, but usually it's a key/value object. For example, the message list tutorial uses the context to work with the loading flags and the channel class instance. 12 | 13 | :::note 14 | The `VirtuosoMessageList` second type parameter is the type of the `context` prop. 15 | ::: 16 | 17 | The example below has a header component that accesses the `loading` flag from the context. The same approach works for all custom components. 18 | 19 | ```tsx live 20 | import { VirtuosoMessageList, VirtuosoMessageListProps, VirtuosoMessageListLicense } from '@virtuoso.dev/message-list' 21 | import { useState } from 'react' 22 | 23 | interface MessageListContext { 24 | loading: boolean 25 | } 26 | 27 | const Header: VirtuosoMessageListProps['Header'] = ({ context }) => ( 28 |
Header - {context.loading ? 'loading' : 'loaded'}
29 | ) 30 | 31 | export default function App() { 32 | const [loading, setLoading] = useState(false) 33 | 34 | return ( 35 | <> 36 | 37 | 38 | 39 | context={{ loading }} 40 | Header={Header} 41 | style={{ height: 500 }} 42 | initialData={Array.from({ length: 100 }, (_, index) => index)} 43 | /> 44 | 45 | 46 | ) 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/custom-scrollbar.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: virtuoso-message-list-custom-scrollbar 3 | title: Custom Scrollbar Component 4 | sidebar_label: Custom Scrollbar Component 5 | sidebar_position: 30 6 | slug: /virtuoso-message-list/custom-scrollbar 7 | --- 8 | 9 | # Custom Scrollbar Component 10 | 11 | Similar to the standard Virtuoso component, `VirtuosoMessageList` supports integration with custom scrollbar components. The custom scrollbar must expose a `ref` to its scrollable element, which is passed through the `customScrollParent` prop. The example above demonstrates how to integrate [SimpleBar](https://github.com/Grsmto/simplebar), a popular custom scrollbar library, with `VirtuosoMessageList`. 12 | 13 | ```tsx live 14 | import { VirtuosoMessageListLicense, VirtuosoMessageList } from '@virtuoso.dev/message-list' 15 | import SimpleBar from 'simplebar-react' 16 | import 'simplebar-react/dist/simplebar.min.css' 17 | import { useState } from 'react' 18 | 19 | export default function App() { 20 | const [scrollParent, setScrollParent] = useState(null) 21 | return ( 22 | 23 | 24 | {scrollParent && ( 25 | index)} /> 26 | )} 27 | 28 | 29 | ) 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/examples/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Examples" 2 | position: 3 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: virtuoso-message-list-hooks 3 | title: Virtuoso Message List Hooks 4 | sidebar_label: Hooks 5 | sidebar_position: 7 6 | slug: /virtuoso-message-list/hooks 7 | --- 8 | 9 | # Hooks 10 | 11 | You can access the message list's internal state and methods by calling hooks inside the inner components. The message list component exposes the following hooks: 12 | 13 | * `useVirtuosoMethods` - the equivalent of the `ref` prop, which gives you access to the message list methods. 14 | * `useVirtuosoLocation` - gives you access to the current `ListScrollLocation`. 15 | * `useCurrentlyRenderedData` - gives you access to the currently rendered data items. Useful if you want to display information for the first or last rendered item. 16 | 17 | :::info 18 | The `useVirtuosoMethods` hook exposes several data operations. To make it type-safe, you should pass the type of data and the context you're using in the message list as a type parameters. 19 | 20 | ```tsx 21 | // For a message list with items of type { message: string } and context of type { user: string } 22 | const methods = useVirtuosoMethods<{message: string}, {user: string}>() 23 | ``` 24 | ::: 25 | 26 | Both hooks are used in the tutorial to implement [the scroll to bottom button](/virtuoso-message-list/tutorial/scroll-to-bottom-button). 27 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/item-keys.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: virtuoso-message-list-item-keys 3 | title: Virtuoso Message List Item Keys 4 | sidebar_label: Item Keys 5 | sidebar_position: 6 6 | slug: /virtuoso-message-list/item-keys 7 | --- 8 | 9 | # Item Keys 10 | 11 | React uses a unique key to identify each item in a list. By default, the Message List component uses the items numeric index for that, but this index might change as we load new messages or delete some. To address that, the message list exposes a `computeItemKey` prop. This prop should return a unique key for each item based on the item data. 12 | 13 | 14 | ```tsx live 15 | import { VirtuosoMessageList, VirtuosoMessageListLicense } from '@virtuoso.dev/message-list' 16 | 17 | export default function App() { 18 | 19 | return ( 20 | 21 | { 23 | return `item-${data}` 24 | }} 25 | style={{ height: '100%' }} 26 | initialData={Array.from({ length: 100 }, (_, index) => index)} 27 | /> 28 | 29 | ) 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/resize-observer-errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: virtuoso-message-list-resize-observer-errors 3 | title: Virtuoso Message Resize Observer Errors 4 | sidebar_label: Resize Observer Errors 5 | sidebar_position: 10 6 | slug: /virtuoso-message-list/resize-observer-errors 7 | --- 8 | 9 | # Resize Observer Errors 10 | 11 | The Message List component uses the Resize Observer API to measure the size of its items. The observer report is handled synchronously, which causes Safari, Webpack in dev mode, or error trackers like Sentry 12 | to catch errors like `ResizeObserver loop limit exceeded` or `ResizeObserver loop completed with undelivered notifications`. Those errors are benign and can be safely ignored. 13 | 14 | ## Ignore Resize Observer Errors in Webpack (dev mode) 15 | 16 | To ignore Resize Observer errors in Webpack dev server overlay, you can add the following code to your Webpack configuration. More details can be found in the [Webpack documentation](https://webpack.js.org/configuration/dev-server/#overlay). 17 | 18 | ```javascript 19 | { 20 | //... 21 | devServer: { 22 | client: { 23 | overlay: { 24 | errors: false, 25 | warnings: false, 26 | runtimeErrors: (error: Error) => { 27 | if (error.message.includes("ResizeObserver loop")) { 28 | return false; 29 | } else { 30 | return true; 31 | } 32 | }, 33 | }, 34 | }, 35 | }, 36 | } 37 | ``` 38 | 39 | ## Ignore Resize Observer Errors in Sentry 40 | 41 | [The following Sentry blog post](https://sentry.io/answers/react-resizeobserver-loop-completed-with-undelivered-notifications/) explains how to ignore Resize Observer errors in Sentry. 42 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/scrolling-to-item.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: virtuoso-message-list-scroll-to-item 3 | title: Virtuoso Message List Scroll to Item 4 | sidebar_label: Scroll to Item 5 | sidebar_position: 7 6 | slug: /virtuoso-message-list/scroll-to-item 7 | --- 8 | 9 | # Scroll to Item 10 | 11 | The message list exposes an imperative API to scroll to a specific item. This is useful when you want to scroll to a specific message or a specific index in the list. 12 | 13 | :::caution 14 | If you want to change the scroll location when updating the data of the list, make sure to use the respective data parameters instead. They are called at the right timing and will not cause unnecessary re-renders and incorrect scroll position due to changed item sizes. 15 | ::: 16 | 17 | ```tsx live 18 | import { VirtuosoMessageList, VirtuosoMessageListMethods, VirtuosoMessageListLicense } from '@virtuoso.dev/message-list' 19 | import React from 'react' 20 | 21 | export default function App() { 22 | const ref = React.useRef(null) 23 | const offset = React.useRef(100) 24 | 25 | return ( 26 | <> 27 | {' '} 34 | 35 | index)} /> 36 | 37 | 38 | ) 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso-message-list/tutorial/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Tutorial" 2 | position: 2 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Virtuoso" 2 | position: 1 3 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso/auto-resizing.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: auto-resizing 3 | title: Auto Resizing Virtual List 4 | sidebar_label: Auto Resizing 5 | slug: /auto-resizing/ 6 | sidebar_position: 2 7 | --- 8 | 9 | The Virtuoso component automatically handles any changes of the items' heights (due to content resizing, images loading, etc.) 10 | You don't have to configure anything additional. 11 | 12 | The list below is wrapped in a resizeable container. Try resizing the container to see how the list reacts. 13 | 14 | ```tsx live 15 | import { Virtuoso } from 'react-virtuoso' 16 | import { useMemo } from 'react' 17 | 18 | export default function App() { 19 | const users = useMemo(() => { 20 | return Array.from({ length: 100000 }, (_, index) => ({ 21 | name: `User ${index}`, 22 | description: `Description for user ${index}` 23 | })) 24 | }, []) 25 | 26 | return ( 27 |
28 | ( 32 |
38 |

{user.name}

39 |
{user.description}
40 |
41 | )} 42 | /> 43 |
44 | ) 45 | } 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso/footer.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: footer 3 | title: List with Footer Example 4 | sidebar_label: Footer 5 | slug: /footer/ 6 | sidebar_position: 5 7 | --- 8 | 9 | Customize the Virtuoso component rendering by passing components through the `components` property. 10 | 11 | For example, the `Footer` component will render at the bottom of the list. 12 | The footer can be used for loading indicators or "load more" buttons. 13 | 14 | Scroll to the bottom of the list to see `end reached`. 15 | 16 | :::note 17 | If you pass the components inline and combine that with `useState`, each re-render will pass a fresh instance component, causing unnecessary unmounting and remounting. 18 | Don't do 19 | 20 | ```tsx 21 |
}} /> 22 | ``` 23 | 24 | Instead, move the components to the module level. If you need to control the components state, pass the necessary variables through Virtuoso's `context` prop. 25 | ::: 26 | 27 | ```tsx live 28 | import { Virtuoso } from 'react-virtuoso' 29 | 30 | function Footer() { 31 | return (
end reached
) 32 | } 33 | 34 | export default function App() { 35 | return ( 36 | (
Item {index}
)} 41 | /> 42 | ) 43 | } 44 | 45 | 46 | ``` 47 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso/horizontal-mode.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: horizontal-mode 3 | title: Horizontal Mode 4 | sidebar_title: Horizontal Mode 5 | slug: /horizontal-mode/ 6 | sidebar_position: 90 7 | --- 8 | 9 | 10 | Setting the `horizontalDirection` property to `true` will render the Virtuoso component horizontally. The items are positioned using `display: inline-block`. 11 | 12 | 13 | ## Horizontal mode list 14 | 15 | ```tsx live 16 | import { Virtuoso } from 'react-virtuoso' 17 | import { useMemo } from 'react' 18 | 19 | export default function App() { 20 | const users = useMemo(() => { 21 | return Array.from({ length: 1000 }, (_, index) => ({ 22 | name: `User ${index}`, 23 | size: Math.floor(Math.random() * 40) + 100, 24 | description: `Description for user ${index}`, 25 | })); 26 | }, []); 27 | 28 | return ( 29 | ( 34 |
41 |
42 |

43 | {user.name} 44 |

45 |
{user.description}
46 |
47 |
48 | )} 49 | /> 50 | ); 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso/initial-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: initial-index 3 | title: Start from a certain item 4 | sidebar_label: Initial Index 5 | slug: /initial-index/ 6 | sidebar_position: 4 7 | --- 8 | 9 | The `initialTopMostItemIndex` property changes the initial location of the list to display the item at the specified index. You can pass in an object to achieve additional effects similar to [scrollToIndex](/scroll-to-index/). 10 | 11 | Note: The property applies to the list only when the component mounts. 12 | If you want to change the position of the list afterward, use the [scrollToIndex](/scroll-to-index/) method. 13 | 14 | ```tsx live 15 | import { Virtuoso } from 'react-virtuoso' 16 | 17 | export default function App() { 18 | return ( 19 | (
Item {index}
)} 24 | /> 25 | ) 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso/range-change-callback.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: range-change-callback 3 | title: Range Change Callback 4 | sidebar_label: Range Change Callback 5 | slug: /range-change-callback/ 6 | sidebar_position: 101 7 | --- 8 | 9 | The `rangeChanged` callback property is called with the start/end indexes of the visible range. 10 | 11 | Note: the `rangeChanged` reports the rendered items, which are affected by the `increaseViewportBy` and the `overscan` properties. 12 | If you must track only the visible items, you can try the workaround from [this Github issue](https://github.com/petyosi/react-virtuoso/issues/118#issuecomment-642156138). 13 | 14 | ```tsx live 15 | import { Virtuoso } from 'react-virtuoso' 16 | import { useState } from 'react' 17 | 18 | export default function App() { 19 | const [visibleRange, setVisibleRange] = useState({ 20 | startIndex: 0, 21 | endIndex: 0, 22 | }) 23 | return ( 24 |
25 |

26 | current visible range: {visibleRange.startIndex} - {visibleRange.endIndex}{' '} 27 |

28 | (
Item {index}
)} 33 | /> 34 |
35 | ) 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso/top-items.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: top-items 3 | title: Top Items List Example 4 | sidebar_label: Top Items 5 | slug: /top-items/ 6 | --- 7 | 8 | The Virtuoso component accepts an optional `topItemCount` number property that allows you to pin the first `N` items of the list. 9 | 10 | Scroll the list below - the first two items remain fixed and always visible. 11 | `backgroundColor` is set to hide the scrollable items behind the top ones. 12 | 13 | ```tsx live 14 | import { Virtuoso } from 'react-virtuoso' 15 | 16 | export default function App() { 17 | return ( 18 | (
Item {index + 1}
)} 23 | /> 24 | ) 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/guides/virtuoso/window-scrolling.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: window-scrolling 3 | title: Window Scrolling 4 | sidebar_label: Window Scrolling 5 | slug: /window-scrolling/ 6 | sidebar_position: 250 7 | --- 8 | 9 | The `Virtuoso` components can use the document scroller by setting the `useWindowScroll` property to `true`. 10 | 11 | ```tsx live 12 | import { Virtuoso } from 'react-virtuoso' 13 | 14 | export default function App() { 15 | return ( 16 | (
Item {index}
)} 20 | /> 21 | ) 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/docs/virtuoso-masonry-api/variables/VirtuosoMasonry.md: -------------------------------------------------------------------------------- 1 | # Variable: VirtuosoMasonry() 2 | 3 | > `const` **VirtuosoMasonry**: \<`Data`, `Context`\>(`props`) => `React.ReactElement` 4 | 5 | ## Type Parameters 6 | 7 | ### Data 8 | 9 | `Data` 10 | 11 | ### Context 12 | 13 | `Context` 14 | 15 | ## Parameters 16 | 17 | ### props 18 | 19 | [`VirtuosoMasonryProps`](../interfaces/VirtuosoMasonryProps.md)\<`Data`, `Context`\> & `object` 20 | 21 | ## Returns 22 | 23 | `React.ReactElement` 24 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs' 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | guidesSidebar: [{ type: 'autogenerated', dirName: 'guides' }], 16 | virtuosoApiSidebar: [ 17 | { 18 | type: 'category', 19 | collapsible: false, 20 | label: 'Virtuoso API', 21 | link: { 22 | type: 'doc', 23 | id: 'virtuoso-api/index', 24 | }, 25 | items: require('./docs/virtuoso-api/typedoc-sidebar.cjs'), 26 | }, 27 | ], 28 | messageListApiSidebar: [ 29 | { 30 | type: 'category', 31 | collapsible: false, 32 | label: 'Virtuoso Message List API', 33 | link: { 34 | type: 'doc', 35 | id: 'virtuoso-message-list-api/index', 36 | }, 37 | items: require('./docs/virtuoso-message-list-api/typedoc-sidebar.cjs'), 38 | }, 39 | ], 40 | masonryApiSidebar: [ 41 | { 42 | type: 'category', 43 | collapsible: false, 44 | label: 'Virtuoso Masonry API', 45 | link: { 46 | type: 'doc', 47 | id: 'virtuoso-masonry-api/index', 48 | }, 49 | items: require('./docs/virtuoso-masonry-api/typedoc-sidebar.cjs'), 50 | }, 51 | ], 52 | /* 53 | messageListApiSidebar: [ 54 | { type: "autogenerated", dirName: "virtuoso-message-list-api" }, 55 | ], 56 | */ 57 | } 58 | 59 | export default sidebars 60 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import Heading from '@theme/Heading' 3 | import styles from './styles.module.css' 4 | 5 | interface FeatureItem { 6 | title: string 7 | description: JSX.Element 8 | } 9 | 10 | const FeatureList: FeatureItem[] = [ 11 | { 12 | title: 'Easy to Use', 13 | description: ( 14 | <>Docusaurus was designed from the ground up to be easily installed and used to get your website up and running quickly. 15 | ), 16 | }, 17 | { 18 | title: 'Focus on What Matters', 19 | description: ( 20 | <> 21 | Docusaurus lets you focus on your docs, and we'll do the chores. Go ahead and move your docs into the docs{' '} 22 | directory. 23 | 24 | ), 25 | }, 26 | { 27 | title: 'Powered by React', 28 | description: ( 29 | <>Extend or customize your website layout by reusing React. Docusaurus can be extended while reusing the same header and footer. 30 | ), 31 | }, 32 | ] 33 | 34 | function Feature({ title, description }: FeatureItem) { 35 | return ( 36 |
37 |
Image
38 |
39 | {title} 40 |

{description}

41 |
42 |
43 | ) 44 | } 45 | 46 | export default function HomepageFeatures(): JSX.Element { 47 | return ( 48 |
49 |
50 |
51 | {FeatureList.map((props, idx) => ( 52 | 53 | ))} 54 |
55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/CodeBlock/Container/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ComponentProps, type ReactNode } from 'react' 2 | import clsx from 'clsx' 3 | import { ThemeClassNames, usePrismTheme } from '@docusaurus/theme-common' 4 | import { getPrismCssVariables } from '@docusaurus/theme-common/internal' 5 | import styles from './styles.module.css' 6 | 7 | export default function CodeBlockContainer({ as: As, ...props }: { as: T } & ComponentProps): ReactNode { 8 | const prismTheme = usePrismTheme() 9 | const prismCssVariables = getPrismCssVariables(prismTheme) 10 | return ( 11 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/CodeBlock/Container/styles.module.css: -------------------------------------------------------------------------------- 1 | .codeBlockContainer { 2 | background-color: var(--gray-2); 3 | color: var(--prism-color); 4 | margin-bottom: var(--ifm-leading); 5 | border-radius: var(--radius-3); 6 | border: 1px solid var(--base-card-classic-border-color); 7 | } 8 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/CodeBlock/Content/Element.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react' 2 | import clsx from 'clsx' 3 | import Container from '@theme/CodeBlock/Container' 4 | import type { Props } from '@theme/CodeBlock/Content/Element' 5 | 6 | import styles from './styles.module.css' 7 | 8 | //
 tags in markdown map to CodeBlocks. They may contain JSX children. When
 9 | // the children is not a simple string, we just return a styled block without
10 | // actually highlighting.
11 | export default function CodeBlockJSX({ children, className }: Props): ReactNode {
12 |   return (
13 |     
14 |       {children}
15 |     
16 |   )
17 | }
18 | 


--------------------------------------------------------------------------------
/apps/virtuoso.dev/src/theme/CodeBlock/CopyButton/styles.module.css:
--------------------------------------------------------------------------------
 1 | :global(.theme-code-block:hover) .copyButtonCopied {
 2 |   opacity: 1 !important;
 3 | }
 4 | 
 5 | .copyButtonIcons {
 6 |   position: relative;
 7 |   width: 1.125rem;
 8 |   height: 1.125rem;
 9 | }
10 | 
11 | .copyButtonIcon,
12 | .copyButtonSuccessIcon {
13 |   position: absolute;
14 |   top: 0;
15 |   left: 0;
16 |   fill: currentColor;
17 |   opacity: inherit;
18 |   width: inherit;
19 |   height: inherit;
20 |   transition: all var(--ifm-transition-fast) ease;
21 | }
22 | 
23 | .copyButtonSuccessIcon {
24 |   top: 50%;
25 |   left: 50%;
26 |   transform: translate(-50%, -50%) scale(0.33);
27 |   opacity: 0;
28 |   color: #00d600;
29 | }
30 | 
31 | .copyButtonCopied .copyButtonIcon {
32 |   transform: scale(0.33);
33 |   opacity: 0;
34 | }
35 | 
36 | .copyButtonCopied .copyButtonSuccessIcon {
37 |   transform: translate(-50%, -50%) scale(1);
38 |   opacity: 1;
39 |   transition-delay: 0.075s;
40 | }
41 | 


--------------------------------------------------------------------------------
/apps/virtuoso.dev/src/theme/CodeBlock/Line/index.tsx:
--------------------------------------------------------------------------------
 1 | import React, { type ReactNode } from 'react'
 2 | import clsx from 'clsx'
 3 | import type { Props } from '@theme/CodeBlock/Line'
 4 | 
 5 | import styles from './styles.module.css'
 6 | 
 7 | export default function CodeBlockLine({ line, classNames, showLineNumbers, getLineProps, getTokenProps }: Props): ReactNode {
 8 |   if (line.length === 1 && line[0].content === '\n') {
 9 |     line[0].content = ''
10 |   }
11 | 
12 |   const lineProps = getLineProps({
13 |     line,
14 |     className: clsx(classNames, showLineNumbers && styles.codeLine),
15 |   })
16 | 
17 |   const lineTokens = line.map((token, key) => )
18 | 
19 |   return (
20 |     
21 |       {showLineNumbers ? (
22 |         <>
23 |           
24 |           {lineTokens}
25 |         
26 |       ) : (
27 |         lineTokens
28 |       )}
29 |       
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/CodeBlock/Line/styles.module.css: -------------------------------------------------------------------------------- 1 | /* Intentionally has zero specificity, so that to be able to override 2 | the background in custom CSS file due bug https://github.com/facebook/docusaurus/issues/3678 */ 3 | :where(:root) { 4 | --docusaurus-highlighted-code-line-bg: rgb(72 77 91); 5 | } 6 | 7 | :where([data-theme='dark']) { 8 | --docusaurus-highlighted-code-line-bg: rgb(100 100 100); 9 | } 10 | 11 | :global(.theme-code-block-highlighted-line) { 12 | background-color: var(--docusaurus-highlighted-code-line-bg); 13 | display: block; 14 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 15 | padding: 0 var(--ifm-pre-padding); 16 | } 17 | 18 | .codeLine { 19 | display: table-row; 20 | counter-increment: line-count; 21 | } 22 | 23 | .codeLineNumber { 24 | display: table-cell; 25 | text-align: right; 26 | width: 1%; 27 | position: sticky; 28 | left: 0; 29 | padding: 0 var(--ifm-pre-padding); 30 | background: var(--ifm-pre-background); 31 | overflow-wrap: normal; 32 | } 33 | 34 | .codeLineNumber::before { 35 | content: counter(line-count); 36 | opacity: 0.4; 37 | } 38 | 39 | :global(.theme-code-block-highlighted-line) .codeLineNumber::before { 40 | opacity: 0.8; 41 | } 42 | 43 | .codeLineContent { 44 | padding-right: var(--ifm-pre-padding); 45 | } 46 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/CodeBlock/LiveCodeBlock/iframe-style.css: -------------------------------------------------------------------------------- 1 | /* 1. Use a more-intuitive box-sizing model */ 2 | *, *::before, *::after { 3 | box-sizing: border-box; 4 | } 5 | 6 | /* 2. Remove default margin */ 7 | * { 8 | margin: 0; 9 | } 10 | 11 | body { 12 | /* 3. Add accessible line-height */ 13 | line-height: 1.5; 14 | /* 4. Improve text rendering */ 15 | -webkit-font-smoothing: antialiased; 16 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI (Custom)', Roboto, 'Helvetica Neue', 'Open Sans (Custom)', system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; 17 | color: var(--foreground); 18 | } 19 | 20 | /* 5. Improve media defaults */ 21 | img, picture, video, canvas, svg { 22 | display: block; 23 | max-width: 100%; 24 | } 25 | 26 | /* 6. Inherit fonts for form controls */ 27 | input, button, textarea, select { 28 | font: inherit; 29 | } 30 | 31 | /* 7. Avoid text overflows */ 32 | p, h1, h2, h3, h4, h5, h6 { 33 | overflow-wrap: break-word; 34 | } 35 | 36 | /* 8. Improve line wrapping */ 37 | p { 38 | text-wrap: pretty; 39 | } 40 | h1, h2, h3, h4, h5, h6 { 41 | text-wrap: balance; 42 | } 43 | 44 | /* 45 | 9. Create a root stacking context 46 | */ 47 | #root, #__next { 48 | isolation: isolate; 49 | } 50 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/CodeBlock/WordWrapButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react' 2 | import clsx from 'clsx' 3 | import { translate } from '@docusaurus/Translate' 4 | import type { Props } from '@theme/CodeBlock/WordWrapButton' 5 | import IconWordWrap from '@theme/Icon/WordWrap' 6 | 7 | import styles from './styles.module.css' 8 | 9 | export default function WordWrapButton({ className, onClick, isEnabled }: Props): ReactNode { 10 | const title = translate({ 11 | id: 'theme.CodeBlock.wordWrapToggle', 12 | message: 'Toggle word wrap', 13 | description: 'The title attribute for toggle word wrapping button of code block lines', 14 | }) 15 | 16 | return ( 17 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/CodeBlock/WordWrapButton/styles.module.css: -------------------------------------------------------------------------------- 1 | .wordWrapButtonIcon { 2 | width: 1.2rem; 3 | height: 1.2rem; 4 | } 5 | 6 | .wordWrapButtonEnabled .wordWrapButtonIcon { 7 | color: var(--ifm-color-primary); 8 | } 9 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/ColorModeToggle/styles.module.css: -------------------------------------------------------------------------------- 1 | .toggle { 2 | width: 2rem; 3 | height: 2rem; 4 | } 5 | 6 | .toggleButton { 7 | -webkit-tap-highlight-color: transparent; 8 | align-items: center; 9 | display: flex; 10 | justify-content: center; 11 | width: 100%; 12 | height: 100%; 13 | border-radius: 50%; 14 | transition: background var(--ifm-transition-fast); 15 | } 16 | 17 | .toggleButton:hover { 18 | background: var(--ifm-color-emphasis-200); 19 | } 20 | 21 | [data-theme='light'] .darkToggleIcon, 22 | [data-theme='dark'] .lightToggleIcon { 23 | display: none; 24 | } 25 | 26 | .toggleButtonDisabled { 27 | cursor: not-allowed; 28 | } 29 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/Icon/ExternalLink/index.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from 'react' 2 | import { ExternalLinkIcon } from '@radix-ui/react-icons' 3 | import type { Props } from '@theme/Icon/ExternalLink' 4 | 5 | import styles from './styles.module.css' 6 | 7 | export default function IconExternalLink({ width = 13.5, height = 13.5 }: Props): ReactNode { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/Icon/ExternalLink/styles.module.css: -------------------------------------------------------------------------------- 1 | .iconExternalLink { 2 | margin-left: 0.3rem; 3 | } 4 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/Layout/Provider/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, type ReactNode } from 'react' 2 | import { composeProviders, useColorMode } from '@docusaurus/theme-common' 3 | import { 4 | ColorModeProvider, 5 | AnnouncementBarProvider, 6 | ScrollControllerProvider, 7 | NavbarProvider, 8 | PluginHtmlClassNameProvider, 9 | } from '@docusaurus/theme-common/internal' 10 | import { DocsPreferredVersionContextProvider } from '@docusaurus/plugin-content-docs/client' 11 | import type { Props } from '@theme/Layout/Provider' 12 | import { Theme } from '@radix-ui/themes' 13 | 14 | const RadixThemeProvider: FC<{ children: ReactNode }> = ({ children }) => { 15 | const { colorMode } = useColorMode() 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | 23 | const Provider = composeProviders([ 24 | ColorModeProvider, 25 | RadixThemeProvider, 26 | AnnouncementBarProvider, 27 | ScrollControllerProvider, 28 | DocsPreferredVersionContextProvider, 29 | PluginHtmlClassNameProvider, 30 | NavbarProvider, 31 | ]) 32 | 33 | export default function LayoutProvider({ children }: Props): ReactNode { 34 | return {children} 35 | } 36 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/src/theme/Navbar/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from 'react' 2 | import Logo from './logo.svg' 3 | import { Box } from '@radix-ui/themes' 4 | 5 | export default function NavbarLogo(): ReactNode { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petyosi/react-virtuoso/820b92a545a38a55e4b58e60df11f6ffe61d36cb/apps/virtuoso.dev/static/.nojekyll -------------------------------------------------------------------------------- /apps/virtuoso.dev/static/CNAME: -------------------------------------------------------------------------------- 1 | virtuoso.dev 2 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/static/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petyosi/react-virtuoso/820b92a545a38a55e4b58e60df11f6ffe61d36cb/apps/virtuoso.dev/static/img/avatar.png -------------------------------------------------------------------------------- /apps/virtuoso.dev/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petyosi/react-virtuoso/820b92a545a38a55e4b58e60df11f6ffe61d36cb/apps/virtuoso.dev/static/img/favicon.ico -------------------------------------------------------------------------------- /apps/virtuoso.dev/static/img/full-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petyosi/react-virtuoso/820b92a545a38a55e4b58e60df11f6ffe61d36cb/apps/virtuoso.dev/static/img/full-logo.png -------------------------------------------------------------------------------- /apps/virtuoso.dev/static/img/navbar-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petyosi/react-virtuoso/820b92a545a38a55e4b58e60df11f6ffe61d36cb/apps/virtuoso.dev/static/img/navbar-bg.png -------------------------------------------------------------------------------- /apps/virtuoso.dev/static/img/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petyosi/react-virtuoso/820b92a545a38a55e4b58e60df11f6ffe61d36cb/apps/virtuoso.dev/static/img/social-card.png -------------------------------------------------------------------------------- /apps/virtuoso.dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "strictNullChecks": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/tsconfig.masonry.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2023", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["../../node_modules/@virtuoso.dev/masonry/dist/index.d.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/virtuoso.dev/tsconfig.message-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2023", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": [ 26 | "../../node_modules/@virtuoso.dev/message-list/dist/index.d.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtuoso.dev/monorepo", 3 | "private": true, 4 | "scripts": { 5 | "ci": "turbo ci-setup && turbo react-virtuoso#build && turbo typecheck lint test e2e", 6 | "build": "turbo build", 7 | "build-virtuoso.dev": "turbo build --filter @virtuoso.dev/virtuoso.dev", 8 | "release": "turbo build && npx @changesets/cli publish", 9 | "changeset-add": "npx @changesets/cli add", 10 | "dev:docs": "turbo run dev @virtuoso.dev/virtuoso.dev#dev", 11 | "dev": "turbo watch dev" 12 | }, 13 | "devDependencies": { 14 | "@changesets/cli": "^2.27.12", 15 | "turbo": "^2.4.4" 16 | }, 17 | "engines": { 18 | "node": "22" 19 | }, 20 | "packageManager": "npm@10.8.2", 21 | "workspaces": [ 22 | "packages/*", 23 | "apps/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/gurx/.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | -------------------------------------------------------------------------------- /packages/gurx/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtuoso.dev/gurx", 3 | "entries": [ 4 | { 5 | "version": "1.0.0", 6 | "tag": "@virtuoso.dev/gurx_v1.0.0", 7 | "date": "Sat, 04 Jan 2025 18:09:03 GMT", 8 | "comments": { 9 | "major": [ 10 | { 11 | "comment": "Initial version publish", 12 | "author": "Petyo Ivanov ", 13 | "commit": "536fb126e41598e455139706f6f3ebce795bde15" 14 | } 15 | ] 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/gurx/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - @virtuoso.dev/gurx 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 59ba51f: Support legacy react 8 | 9 | This log was last generated on Sat, 04 Jan 2025 18:09:03 GMT and should not be manually modified. 10 | 11 | ## 1.0.0 12 | 13 | Sat, 04 Jan 2025 18:09:03 GMT 14 | 15 | ### Breaking changes 16 | 17 | - Initial version publish 18 | -------------------------------------------------------------------------------- /packages/gurx/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Petyo Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/gurx/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from '@virtuoso.dev/tooling/eslint.config' 2 | 3 | export default [ 4 | ...baseConfig, 5 | { 6 | languageOptions: { 7 | parserOptions: { 8 | projectService: true, 9 | tsconfigRootDir: import.meta.dirname, 10 | }, 11 | }, 12 | }, 13 | ] 14 | -------------------------------------------------------------------------------- /packages/gurx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtuoso.dev/gurx", 3 | "private": false, 4 | "sideEffects": false, 5 | "type": "module", 6 | "version": "1.1.0", 7 | "module": "dist/index.js", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc && vite build", 12 | "test": "vitest", 13 | "ci-lint": "eslint", 14 | "lint": "eslint", 15 | "typecheck": "tsc --noEmit" 16 | }, 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/petyosi/react-virtuoso.git" 23 | }, 24 | "license": "MIT", 25 | "peerDependencies": { 26 | "react": ">= 16 || >= 17 || >= 18 || >= 19", 27 | "react-dom": ">= 16 || >= 17 || >= 18 || >= 19" 28 | }, 29 | "devDependencies": { 30 | "@microsoft/api-extractor": "^7.48.0", 31 | "@types/node": "^22.10.1", 32 | "@types/react": "^18.3.12", 33 | "@types/react-dom": "^18.3.1", 34 | "@virtuoso.dev/tooling": "*", 35 | "@vitejs/plugin-react-swc": "^3.7.2", 36 | "@vitest/browser": "3.1.1", 37 | "eslint": "^9.24.0", 38 | "playwright": "^1.49.0", 39 | "prettier": "^3.5.3", 40 | "react": "^18.3.1", 41 | "react-dom": "^18.3.1", 42 | "typescript": "~5.7.2", 43 | "vite": "^6.2.6", 44 | "vite-plugin-dts": "~4.4.0", 45 | "vitest": "3.1.1", 46 | "vitest-browser-react": "^0.1.1" 47 | }, 48 | "files": [ 49 | "dist", 50 | "CHANGELOG.md" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /packages/gurx/prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from '@virtuoso.dev/tooling/prettier.config' 2 | 3 | export default baseConfig 4 | -------------------------------------------------------------------------------- /packages/gurx/src/AsyncQuery.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, type PipeRef } from './realm' 2 | 3 | interface QueryLoadingResult { 4 | data: null 5 | error: null 6 | isLoading: true 7 | type: 'loading' 8 | } 9 | 10 | interface QuerySuccessResult { 11 | data: T 12 | error: null 13 | isLoading: false 14 | type: 'success' 15 | } 16 | 17 | interface QueryErrorResult { 18 | data: null 19 | error: Error 20 | isLoading: false 21 | type: 'error' 22 | } 23 | 24 | type QueryResult = QueryErrorResult | QueryLoadingResult | QuerySuccessResult 25 | 26 | const INITIAL_QUERY_RESULT: QueryLoadingResult = { data: null, error: null, isLoading: true, type: 'loading' } 27 | 28 | export function AsyncQuery(query: (params: I) => Promise, defaultParams: I): PipeRef> { 29 | return Pipe(INITIAL_QUERY_RESULT as QueryResult, (r, input$, output$) => { 30 | function runQuery(params: I) { 31 | r.pub(output$, INITIAL_QUERY_RESULT) 32 | query(params) 33 | .then((data) => { 34 | r.pub(output$, { data, error: null, isLoading: false, type: 'success' }) 35 | }) 36 | .catch((error: unknown) => { 37 | r.pub(output$, { data: null, error, isLoading: false, type: 'error' }) 38 | }) 39 | } 40 | runQuery(defaultParams) 41 | r.sub(input$, runQuery) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /packages/gurx/src/RefCount.ts: -------------------------------------------------------------------------------- 1 | export class RefCount { 2 | constructor(readonly map = new Map()) {} 3 | 4 | clone() { 5 | return new RefCount(new Map(this.map)) 6 | } 7 | 8 | decrement(id: symbol, ifZero: () => void) { 9 | let counter = this.map.get(id) 10 | if (counter !== undefined) { 11 | counter -= 1 12 | this.map.set(id, counter) 13 | if (counter === 0) { 14 | ifZero() 15 | } 16 | } 17 | } 18 | 19 | increment(id: symbol) { 20 | const counter = this.map.get(id) ?? 0 21 | this.map.set(id, counter + 1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/gurx/src/SetMap.ts: -------------------------------------------------------------------------------- 1 | export class SetMap { 2 | map = new Map>() 3 | 4 | delete(id: symbol) { 5 | return this.map.delete(id) 6 | } 7 | 8 | get(id: symbol) { 9 | return this.map.get(id) 10 | } 11 | 12 | getOrCreate(id: symbol) { 13 | let record = this.map.get(id) 14 | if (record === undefined) { 15 | record = new Set() 16 | this.map.set(id, record) 17 | } 18 | return record 19 | } 20 | 21 | use(id: symbol, cb: (value: Set) => unknown) { 22 | const set = this.get(id) 23 | if (set !== undefined) { 24 | cb(set) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/gurx/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AsyncQuery' 2 | export * from './hooks' 3 | export * from './operators' 4 | export { RealmContext, RealmProvider } from './react' 5 | export * from './realm' 6 | -------------------------------------------------------------------------------- /packages/gurx/src/react.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Realm } from './realm' 4 | 5 | /** 6 | * @category React Components 7 | * The context that provides the realm to the built-in hooks. 8 | */ 9 | export const RealmContext = React.createContext(null) 10 | 11 | /** 12 | * @category React Components 13 | */ 14 | export function RealmProvider({ 15 | children, 16 | initWith, 17 | updateWith = {}, 18 | }: { 19 | /** 20 | * The children to render 21 | */ 22 | children: React.ReactNode 23 | /** 24 | * The initial values to set in the realm 25 | */ 26 | initWith?: Record 27 | /** 28 | * The values to update in the realm on each render 29 | */ 30 | updateWith?: Record 31 | }) { 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | const theRealm = React.useMemo(() => new Realm(initWith), []) 34 | 35 | React.useEffect(() => { 36 | theRealm.pubIn(updateWith) 37 | }, [updateWith, theRealm]) 38 | 39 | return {children} 40 | } 41 | -------------------------------------------------------------------------------- /packages/gurx/src/test/browser/__screenshots__/react.test.tsx/Gurx-React-Realm-renders-a-cell-value-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petyosi/react-virtuoso/820b92a545a38a55e4b58e60df11f6ffe61d36cb/packages/gurx/src/test/browser/__screenshots__/react.test.tsx/Gurx-React-Realm-renders-a-cell-value-1.png -------------------------------------------------------------------------------- /packages/gurx/src/test/node/AsyncQuery.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { AsyncQuery, Realm } from '../..' 4 | 5 | describe('AsyncQuery', () => { 6 | it('should work', async () => { 7 | const query$ = AsyncQuery(async (params: number) => { 8 | return new Promise((resolve) => { 9 | setTimeout(() => { 10 | resolve(`hello ${params}`) 11 | }, 50) 12 | }) 13 | }, 2) 14 | 15 | const r = new Realm() 16 | const sub = vi.fn() 17 | r.sub(query$, sub) 18 | expect(r.getValue(query$)).toMatchObject({ data: null, error: null, isLoading: true, type: 'loading' }) 19 | await new Promise((resolve) => setTimeout(resolve, 100)) 20 | expect(sub).toHaveBeenCalledTimes(1) 21 | expect(sub).toHaveBeenCalledWith({ data: 'hello 2', error: null, isLoading: false, type: 'success' }) 22 | r.pub(query$, 4) 23 | expect(sub).toHaveBeenCalledTimes(2) 24 | expect(sub).toHaveBeenCalledWith({ data: null, error: null, isLoading: true, type: 'loading' }) 25 | await new Promise((resolve) => setTimeout(resolve, 100)) 26 | expect(sub).toHaveBeenCalledWith({ data: 'hello 4', error: null, isLoading: false, type: 'success' }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/gurx/src/test/node/RefCount.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { RefCount } from '../../RefCount' 4 | 5 | describe('ref counter', () => { 6 | it('increments by one', () => { 7 | const counter = new RefCount() 8 | const key = Symbol() 9 | counter.increment(key) 10 | expect(counter.map.get(key)).toEqual(1) 11 | counter.increment(key) 12 | expect(counter.map.get(key)).toEqual(2) 13 | }) 14 | 15 | it('decrements by one', () => { 16 | const cb = vi.fn() 17 | const counter = new RefCount() 18 | const key = Symbol() 19 | counter.increment(key) 20 | expect(counter.map.get(key)).toEqual(1) 21 | counter.increment(key) 22 | expect(counter.map.get(key)).toEqual(2) 23 | counter.decrement(key, cb) 24 | expect(counter.map.get(key)).toEqual(1) 25 | counter.decrement(key, cb) 26 | expect(cb).toHaveBeenCalledTimes(1) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/gurx/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calls callback with the first argument, and returns it. 3 | */ 4 | export function tap(arg: T, callback: (arg: T) => unknown): T { 5 | callback(arg) 6 | return arg 7 | } 8 | 9 | export function noop() { 10 | // do nothing 11 | } 12 | -------------------------------------------------------------------------------- /packages/gurx/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/gurx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@virtuoso.dev/tooling/tsconfig.base.json", 4 | "include": ["src", "vite.config.ts", "eslint.config.mjs", "prettier.config.mjs", "vitest.workspace.ts"], 5 | "compilerOptions": { 6 | "types": ["@vitest/browser/matchers"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/gurx/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "jsx": "react-jsx", 9 | "strictNullChecks": true 10 | }, 11 | "include": ["vite.config.ts", "vitest.workspace.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/gurx/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc' 2 | /// 3 | import { resolve } from 'node:path' 4 | import { defineConfig } from 'vite' 5 | import dts from 'vite-plugin-dts' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | build: { 10 | lib: { 11 | entry: resolve(__dirname, 'src/index.ts'), 12 | fileName: 'index', 13 | formats: ['es'], 14 | }, 15 | rollupOptions: { 16 | external: ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime'], 17 | output: { exports: 'named' }, 18 | }, 19 | }, 20 | plugins: [ 21 | ...react(), 22 | dts({ 23 | compilerOptions: { skipLibCheck: true }, 24 | rollupTypes: true, 25 | staticImport: true, 26 | }), 27 | ], 28 | }) 29 | -------------------------------------------------------------------------------- /packages/gurx/vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from 'vitest/config' 2 | 3 | export default defineWorkspace([ 4 | { 5 | test: { 6 | environment: 'node', 7 | include: ['src/test/node/*.test.ts'], 8 | name: 'node', 9 | }, 10 | }, 11 | { 12 | test: { 13 | browser: { 14 | enabled: true, 15 | instances: [{ browser: 'chromium' }], 16 | name: 'chromium', 17 | provider: 'playwright', 18 | }, 19 | // setupFiles: ['vitest-browser-react'], 20 | include: ['src/test/browser/*.test.tsx'], 21 | name: 'browser', 22 | }, 23 | }, 24 | ]) 25 | -------------------------------------------------------------------------------- /packages/masonry/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtuoso.dev/masonry", 3 | "entries": [ 4 | { 5 | "version": "1.3.2", 6 | "tag": "@virtuoso.dev/masonry_v1.3.2", 7 | "date": "Fri, 10 Jan 2025 08:23:07 GMT", 8 | "comments": { 9 | "patch": [ 10 | { 11 | "comment": "Switch to @virtuoso.dev/gurx", 12 | "author": "Petyo Ivanov ", 13 | "commit": "b303ff5d9992ac6f25d9005b654741218b841d78" 14 | } 15 | ] 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/masonry/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - @virtuoso.dev/masonry 2 | 3 | ## 1.3.3 4 | 5 | ### Patch Changes 6 | 7 | - 7e85d9e: Support react portal to an iframe rendering 8 | 9 | This log was last generated on Fri, 10 Jan 2025 08:23:07 GMT and should not be manually modified. 10 | 11 | ## 1.3.2 12 | 13 | Fri, 10 Jan 2025 08:23:07 GMT 14 | 15 | ### Patches 16 | 17 | - Switch to @virtuoso.dev/gurx 18 | -------------------------------------------------------------------------------- /packages/masonry/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Petyo Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/masonry/README.md: -------------------------------------------------------------------------------- 1 | # React Virtuoso Masonry 2 | -------------------------------------------------------------------------------- /packages/masonry/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from '@virtuoso.dev/tooling/eslint.config' 2 | 3 | export default [ 4 | ...baseConfig, 5 | { 6 | languageOptions: { 7 | parserOptions: { 8 | projectService: true, 9 | tsconfigRootDir: import.meta.dirname, 10 | }, 11 | }, 12 | }, 13 | ] 14 | -------------------------------------------------------------------------------- /packages/masonry/prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from '@virtuoso.dev/tooling/prettier.config' 2 | 3 | export default baseConfig 4 | -------------------------------------------------------------------------------- /packages/masonry/src/content.tsx: -------------------------------------------------------------------------------- 1 | import { Cell } from '@virtuoso.dev/gurx' 2 | 3 | import type { ItemContent } from './interfaces' 4 | 5 | export const DefaultItemContent: ItemContent = ({ index }) => { 6 | return
Item {index}
7 | } 8 | 9 | export const itemContent$ = Cell(DefaultItemContent) 10 | -------------------------------------------------------------------------------- /packages/masonry/src/data.ts: -------------------------------------------------------------------------------- 1 | import { Cell, filter, link, map, pipe } from '@virtuoso.dev/gurx' 2 | 3 | import type { Data } from './interfaces' 4 | 5 | export const totalCount$ = Cell(0) 6 | export const context$ = Cell(null) 7 | 8 | export const data$ = Cell(null, () => { 9 | link( 10 | pipe( 11 | data$, 12 | filter((data) => data !== null), 13 | map((data) => data.length) 14 | ), 15 | totalCount$ 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/masonry/src/dom.ts: -------------------------------------------------------------------------------- 1 | import { Cell, combine, DerivedCell, map, pipe } from '@virtuoso.dev/gurx' 2 | 3 | export const scrollTop$ = Cell(0) 4 | 5 | export const viewportHeight$ = Cell(0) 6 | 7 | export const viewportWidth$ = Cell(0) 8 | 9 | export const scrollHeight$ = Cell(0) 10 | 11 | export const listScrollTop$ = scrollTop$ 12 | 13 | export const listOffset$ = Cell(0) 14 | 15 | export const useWindowScroll$ = Cell(false) 16 | 17 | export const visibleListHeight$ = DerivedCell(0, () => { 18 | return pipe( 19 | combine(viewportHeight$, useWindowScroll$, listOffset$), 20 | map(([viewportHeight, useWindowScroll, listOffset]) => { 21 | const result = useWindowScroll ? viewportHeight - Math.max(listOffset, 0) : viewportHeight 22 | return result 23 | }) 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/masonry/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces' 2 | export * from './VirtuosoMasonry' 3 | -------------------------------------------------------------------------------- /packages/masonry/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export interface OffsetPoint { 3 | height: number 4 | index: number 5 | offset: number 6 | } 7 | 8 | /** @internal */ 9 | export interface SizeRange { 10 | endIndex: number 11 | size: number 12 | startIndex: number 13 | } 14 | 15 | /** @internal */ 16 | export type Data = Item[] 17 | 18 | /** 19 | * Used for the custom components that accept the masonry context prop. 20 | */ 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | export type ContextAwareComponent = React.ComponentType<{ 23 | /** 24 | * The value currently passed to the `context` prop of the `VirtuosoMasonry` component. 25 | */ 26 | context: Context 27 | }> 28 | 29 | /** @internal */ 30 | export type ComputeItemKey = (params: { context: Context; data: Data; index: number }) => React.Key 31 | 32 | /** 33 | * The DOM attributes that you can pass to the `VirtuosoMasonry` component to customize the scroll element. 34 | * @noInheritDoc 35 | */ 36 | export type ScrollerProps = Omit, 'data' | 'onScroll' | 'ref'> 37 | 38 | /** 39 | * A React component that's used to render the individual item. 40 | */ 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | export type ItemContent = React.ComponentType<{ 43 | /** 44 | * The value of the `context` prop passed to the list. 45 | */ 46 | context: Context 47 | /** 48 | * The data item to render. 49 | */ 50 | data: Data 51 | /** 52 | * The index of the item in the list data array. 53 | */ 54 | index: number 55 | }> 56 | -------------------------------------------------------------------------------- /packages/masonry/src/sizing/binaryArraySearch.ts: -------------------------------------------------------------------------------- 1 | export type Comparator = (item: T, value: number) => -1 | 0 | 1 2 | 3 | export function findIndexOfClosestSmallerOrEqual(items: T[], value: number, comparator: Comparator, start = 0): number { 4 | let end = items.length - 1 5 | 6 | while (start <= end) { 7 | const index = Math.floor((start + end) / 2) 8 | const item = items[index] 9 | const match = comparator(item, value) 10 | if (match === 0) { 11 | return index 12 | } 13 | 14 | if (match === -1) { 15 | if (end - start < 2) { 16 | return index - 1 17 | } 18 | end = index - 1 19 | } else { 20 | if (end === start) { 21 | return index 22 | } 23 | start = index + 1 24 | } 25 | } 26 | 27 | throw new Error(`Failed binary finding record in array - ${items.join(',')}, searched for ${value}`) 28 | } 29 | 30 | export function findClosestSmallerOrEqual(items: T[], value: number, comparator: Comparator): T { 31 | return items[findIndexOfClosestSmallerOrEqual(items, value, comparator)] 32 | } 33 | 34 | export function findRange(items: T[], startValue: number, endValue: number, comparator: Comparator): T[] { 35 | const startIndex = findIndexOfClosestSmallerOrEqual(items, startValue, comparator) 36 | const endIndex = findIndexOfClosestSmallerOrEqual(items, endValue, comparator, startIndex) 37 | return items.slice(startIndex, endIndex + 1) 38 | } 39 | -------------------------------------------------------------------------------- /packages/masonry/src/sizing/deleteIndices.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { insert, newTree, walk } from './AATree' 4 | import { deleteIndices } from './deleteIndices' 5 | 6 | describe('deleting indices from an AA tree', () => { 7 | it('decreases the indices of the existing tree', () => { 8 | let tree = newTree() 9 | tree = insert(tree, 0, 20) 10 | tree = insert(tree, 10, 30) 11 | tree = insert(tree, 20, 20) 12 | tree = insert(tree, 30, 30) 13 | 14 | const updatedTree = deleteIndices(tree, [1, 3, 5, 15, 25]) 15 | 16 | expect(walk(updatedTree)).toMatchObject([ 17 | { k: 0, v: 20 }, 18 | { k: 7, v: 30 }, 19 | { k: 16, v: 20 }, 20 | { k: 25, v: 30 }, 21 | ]) 22 | }) 23 | it('merges same sized indices', () => { 24 | let tree = newTree() 25 | tree = insert(tree, 0, 20) 26 | tree = insert(tree, 1, 30) 27 | tree = insert(tree, 2, 20) 28 | tree = insert(tree, 3, 30) 29 | tree = insert(tree, 4, 20) 30 | tree = insert(tree, 5, 30) 31 | tree = insert(tree, 6, 20) 32 | 33 | const updatedTree = deleteIndices(tree, [1, 3]) 34 | 35 | expect(walk(updatedTree)).toMatchObject([ 36 | { k: 0, v: 20 }, 37 | { k: 3, v: 30 }, 38 | { k: 4, v: 20 }, 39 | ]) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/masonry/src/sizing/deleteIndices.ts: -------------------------------------------------------------------------------- 1 | import { type AANode, insert, newTree, walk } from './AATree' 2 | 3 | export function deleteIndices(tree: AANode, indices: number[]) { 4 | const remainingIndices = indices.slice() 5 | let shiftAmount = 0 6 | 7 | const newValues: { k: number; v: number }[] = [] 8 | for (const { k, v } of walk(tree)) { 9 | while (remainingIndices.length && remainingIndices[0] < k) { 10 | remainingIndices.shift() 11 | shiftAmount++ 12 | } 13 | 14 | const newKey = Math.max(0, k - shiftAmount) 15 | const prevKey = newValues.at(-1)?.k ?? -1 16 | 17 | // zero-length range, 18 | if (newKey === prevKey) { 19 | const prevPrevValue = newValues.at(-2)?.v ?? -1 20 | if (prevPrevValue === v) { 21 | newValues.pop() 22 | } else { 23 | newValues[newValues.length - 1].v = v 24 | } 25 | } else { 26 | newValues.push({ k: newKey, v }) 27 | } 28 | } 29 | 30 | let resultTree = newTree() 31 | for (const { k, v } of newValues) { 32 | resultTree = insert(resultTree, k, v) 33 | } 34 | return resultTree 35 | } 36 | -------------------------------------------------------------------------------- /packages/masonry/src/sizing/offsetTreeReducer.ts: -------------------------------------------------------------------------------- 1 | import type { OffsetPoint } from '../interfaces' 2 | 3 | import { type AANode, findMaxKeyValue, rangesWithin } from './AATree' 4 | import { findIndexOfClosestSmallerOrEqual } from './binaryArraySearch' 5 | import { indexComparator } from './rangesWithinOffsets' 6 | 7 | export const OFFSET_TREE_SEED = [[], 0, 0, 0] as [OffsetPoint[], number, number, number] 8 | export function offsetTreeReducer(offsetTree: OffsetPoint[], [sizeTree, lastRangeStart]: [AANode, number]) { 9 | let prevIndex = 0 10 | let prevHeight = 0 11 | 12 | let prevOffset = 0 13 | let startAtIndex = 0 14 | 15 | if (lastRangeStart !== 0) { 16 | startAtIndex = findIndexOfClosestSmallerOrEqual(offsetTree, lastRangeStart - 1, indexComparator) 17 | const offsetInfo = offsetTree[startAtIndex] 18 | prevOffset = offsetInfo.offset 19 | const kv = findMaxKeyValue(sizeTree, lastRangeStart - 1) 20 | if (kv[1] === undefined) { 21 | throw new Error('Invariant violation') 22 | } 23 | prevIndex = kv[0] 24 | prevHeight = kv[1] 25 | 26 | if (offsetTree.length && offsetTree[startAtIndex].height === findMaxKeyValue(sizeTree, lastRangeStart)[1]) { 27 | startAtIndex -= 1 28 | } 29 | 30 | offsetTree = offsetTree.slice(0, startAtIndex + 1) 31 | } else { 32 | offsetTree = [] 33 | } 34 | 35 | for (const { start: index, value: height } of rangesWithin(sizeTree, lastRangeStart, Number.POSITIVE_INFINITY)) { 36 | const offset = (index - prevIndex) * prevHeight + prevOffset 37 | offsetTree.push({ height, index, offset }) 38 | prevIndex = index 39 | prevOffset = offset 40 | prevHeight = height 41 | } 42 | 43 | return [offsetTree, prevHeight, prevOffset, prevIndex] as typeof OFFSET_TREE_SEED 44 | } 45 | -------------------------------------------------------------------------------- /packages/masonry/src/sizing/rangesWithinOffsets.ts: -------------------------------------------------------------------------------- 1 | import type { OffsetPoint } from '../interfaces' 2 | 3 | import { arrayToRanges } from './AATree' 4 | import * as arrayBinarySearch from './binaryArraySearch' 5 | 6 | export function indexComparator({ index: itemIndex }: OffsetPoint, index: number) { 7 | return index === itemIndex ? 0 : index < itemIndex ? -1 : 1 8 | } 9 | 10 | export function offsetComparator({ offset: itemOffset }: OffsetPoint, offset: number) { 11 | return offset === itemOffset ? 0 : offset < itemOffset ? -1 : 1 12 | } 13 | 14 | function offsetPointParser(point: OffsetPoint) { 15 | return { index: point.index, value: point } 16 | } 17 | 18 | export function rangesWithinOffsets( 19 | tree: OffsetPoint[], 20 | startOffset: number, 21 | endOffset: number, 22 | minStartIndex = 0 23 | ): { 24 | end: number 25 | start: number 26 | value: OffsetPoint 27 | }[] { 28 | if (minStartIndex > 0) { 29 | startOffset = Math.max(startOffset, arrayBinarySearch.findClosestSmallerOrEqual(tree, minStartIndex, indexComparator).offset) 30 | } 31 | 32 | startOffset = Math.max(0, startOffset) 33 | 34 | return arrayToRanges(arrayBinarySearch.findRange(tree, startOffset, endOffset, offsetComparator), offsetPointParser) 35 | } 36 | -------------------------------------------------------------------------------- /packages/masonry/src/sizing/sizeTreeReducer.ts: -------------------------------------------------------------------------------- 1 | import type { SizeRange } from '../interfaces' 2 | 3 | import { type AANode, empty, insert, newTree } from './AATree' 4 | import { insertRanges } from './insertRanges' 5 | 6 | export const SIZE_TREE_SEED = [newTree(), 0] as [AANode, number] 7 | export function sizeTreeReducer(currentTree: AANode, [ranges, groupIndices]: [SizeRange[], number[]]) { 8 | // We receive probe item results from a group probe, 9 | // which should always pass an item and a group 10 | // the results contain two ranges, which we consider to mean that groups and items have different heights 11 | if (groupIndices.length > 0 && empty(currentTree) && ranges.length === 2) { 12 | const groupSize = ranges[0].size 13 | const itemSize = ranges[1].size 14 | 15 | return [ 16 | groupIndices.reduce((tree, groupIndex) => { 17 | return insert(insert(tree, groupIndex, groupSize), groupIndex + 1, itemSize) 18 | }, newTree()), 19 | 0, 20 | ] as [AANode, number] 21 | } 22 | return insertRanges(currentTree, ranges) 23 | } 24 | -------------------------------------------------------------------------------- /packages/masonry/src/test/node/ssr.test.tsx: -------------------------------------------------------------------------------- 1 | import { parse } from 'node-html-parser' 2 | import { renderToString } from 'react-dom/server' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | import { VirtuosoMasonry, type VirtuosoMasonryProps } from '../../VirtuosoMasonry' 6 | 7 | const ItemContent: VirtuosoMasonryProps['ItemContent'] = ({ data }) =>
{data}
8 | 9 | describe('ssr', () => { 10 | it('works', () => { 11 | const result = renderToString( 12 | index)} 15 | initialItemCount={10} 16 | ItemContent={ItemContent} 17 | /> 18 | ) 19 | const root = parse(result) 20 | expect(root.firstChild?.childNodes.length).toBe(3) 21 | expect(root.firstChild?.childNodes[0].childNodes.length).toBe(4) 22 | expect(root.firstChild?.childNodes[1].childNodes.length).toBe(3) 23 | expect(root.firstChild?.childNodes[2].childNodes.length).toBe(3) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/masonry/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare const PACKAGE_TIMESTAMP: number 3 | -------------------------------------------------------------------------------- /packages/masonry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@virtuoso.dev/tooling/tsconfig.base.json", 4 | "include": ["src", "vite.config.ts", "eslint.config.mjs", "prettier.config.mjs", "vitest.workspace.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/masonry/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": [ 11 | "vite.config.ts", 12 | "vitest.workspace.ts" 13 | ] 14 | } -------------------------------------------------------------------------------- /packages/masonry/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc' 2 | import { resolve } from 'node:path' 3 | /// 4 | import { defineConfig } from 'vite' 5 | import dts from 'vite-plugin-dts' 6 | 7 | const inLadle = process.env.LADLE === 'true' 8 | 9 | const define = { 10 | PACKAGE_TIMESTAMP: new Date().getTime(), 11 | } 12 | // https://vitejs.dev/config/ 13 | export default inLadle 14 | ? defineConfig({ 15 | define, 16 | }) 17 | : defineConfig({ 18 | build: { 19 | lib: { 20 | entry: resolve(__dirname, 'src/index.ts'), 21 | fileName: 'index', 22 | formats: ['es'], 23 | name: 'Virtuoso', 24 | }, 25 | minify: true, 26 | rollupOptions: { 27 | external: ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', '@virtuoso.dev/gurx', '@ladle/react'], 28 | output: { 29 | exports: 'named', 30 | }, 31 | }, 32 | }, 33 | define, 34 | plugins: [ 35 | react(), 36 | dts({ 37 | compilerOptions: { skipLibCheck: true }, 38 | rollupTypes: true, 39 | staticImport: true, 40 | }), 41 | ], 42 | }) 43 | -------------------------------------------------------------------------------- /packages/masonry/vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from 'vitest/config' 2 | 3 | export default defineWorkspace([ 4 | { 5 | test: { 6 | environment: 'node', 7 | include: ['src/test/node/**/*.test.{ts,tsx}'], 8 | name: 'node env', 9 | }, 10 | }, 11 | { 12 | test: { 13 | browser: { 14 | enabled: true, 15 | name: 'chromium', 16 | provider: 'playwright', 17 | }, 18 | // an example of file based convention, 19 | // you don't have to follow it 20 | include: ['src/test/browser/**/*.test.{ts,tsx}'], 21 | name: 'browser', 22 | }, 23 | }, 24 | ]) 25 | -------------------------------------------------------------------------------- /packages/react-virtuoso/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | stories: ['./examples/*.tsx'], 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-virtuoso/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-virtuoso 2 | 3 | ## 4.12.8 4 | 5 | ### Patch Changes 6 | 7 | - dbe93a0: Follow output works with fixedItemHeight set 8 | 9 | ## 4.12.7 10 | 11 | ### Patch Changes 12 | 13 | - a04ba00: Fix for prepend items flickering 14 | 15 | ## 4.12.6 16 | 17 | ### Patch Changes 18 | 19 | - bb0402e: Support window scrolling to iframe react portals 20 | 21 | ## 4.12.5 22 | 23 | ### Patch Changes 24 | 25 | - b1d4519: Revert node requirements 26 | 27 | ## 4.12.4 28 | 29 | ### Patch Changes 30 | 31 | - fdbf0c5: Updated to latest tooling 32 | - fdbf0c5: correct TS types for custom component, context is always passed 33 | -------------------------------------------------------------------------------- /packages/react-virtuoso/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Petyo Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/react-virtuoso/README.md: -------------------------------------------------------------------------------- 1 | # React Virtuoso 2 | 3 | **React Virtuoso** - the most complete React virtualization rendering list/table/grid family of components. 4 | 5 | [virtuoso.dev](https://virtuoso.dev). 6 | 7 | ## License 8 | 9 | MIT License. 10 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/collapsible-long-item.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with collapsible long items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'collapsible-long-item') 9 | await page.waitForSelector('[data-testid=virtuoso-scroller]') 10 | await page.waitForTimeout(500) 11 | }) 12 | 13 | test.skip('compensates correctly when collapsing an item', async ({ page }) => { 14 | await page.waitForSelector('[data-testid=virtuoso-scroller]') 15 | await page.evaluate(() => { 16 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 17 | scroller.scrollBy({ top: -400 }) 18 | }) 19 | 20 | await page.waitForTimeout(500) 21 | await page.waitForSelector('[data-index="90"] button') 22 | 23 | await page.evaluate(() => { 24 | const button = document.querySelector('[data-index="90"] button') as HTMLElement 25 | button.click() 26 | }) 27 | 28 | await page.waitForTimeout(200) 29 | 30 | const scrollTop = await page.evaluate(() => { 31 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 32 | return scroller.scrollTop 33 | }) 34 | 35 | expect(scrollTop).toBe(9200) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/data.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with hundred items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'data') 9 | await page.waitForSelector('[data-testid=virtuoso-scroller]') 10 | await page.waitForTimeout(100) 11 | }) 12 | 13 | test('renders 10 items', async ({ page }) => { 14 | const itemCount = await page.evaluate(() => { 15 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]')! 16 | return listContainer.childElementCount 17 | }) 18 | expect(itemCount).toBe(10) 19 | }) 20 | 21 | test('fills in the scroller', async ({ page }) => { 22 | const scrollHeight = await page.evaluate(() => { 23 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 24 | return scroller.scrollHeight 25 | }) 26 | expect(scrollHeight).toBe(100 * 30) 27 | }) 28 | 29 | test('increases the items', async ({ page }) => { 30 | await page.evaluate(() => { 31 | document.querySelector('button')!.click() 32 | }) 33 | 34 | const scrollHeight = await page.evaluate(() => { 35 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 36 | return scroller.scrollHeight 37 | }) 38 | 39 | expect(scrollHeight).toBe(120 * 30) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/follow-output-loading-image.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with hundred items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'follow-output-loading-image') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('scrolls to bottom when image is loaded', async ({ page }) => { 13 | await page.locator('data-testid=add-image').click() 14 | await page.waitForTimeout(800) 15 | const scrollTop = await page.locator('data-testid=virtuoso-scroller').evaluate((el) => el.scrollTop) 16 | expect(scrollTop).toBe(2800) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/gap.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with hundred items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'gap') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('renders only 6 items', async ({ page }) => { 13 | const itemCount = await page.evaluate(() => { 14 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]')! 15 | return listContainer.childElementCount 16 | }) 17 | expect(itemCount).toBe(6) 18 | }) 19 | 20 | test('fills in the scroller', async ({ page }) => { 21 | const scrollHeight = await page.evaluate(() => { 22 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 23 | return scroller.scrollHeight 24 | }) 25 | expect(scrollHeight).toBe(100 * 32 + 99 * 20) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/grid-gap.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('grid with gaps', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'grid-gap') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('renders 16 items', async ({ page }) => { 13 | const itemCount = await page.evaluate(() => { 14 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]') 15 | return listContainer!.childElementCount 16 | }) 17 | expect(itemCount).toBe(16) 18 | }) 19 | 20 | test('fills in the scroller', async ({ page }) => { 21 | await page.waitForTimeout(100) 22 | const scrollHeight = await page.evaluate(() => { 23 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]') 24 | return scroller!.scrollHeight 25 | }) 26 | expect(scrollHeight).toBe(2480) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/grid-scroll-seek-placeholder.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with scroll seek placeholders', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'grid-scroll-seek-placeholder') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('renders grid placeholders when scrolled', async ({ page }) => { 13 | await page.evaluate(() => { 14 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 15 | setInterval(() => { 16 | scroller.scrollBy({ top: 30 }) 17 | }, 10) 18 | }) 19 | 20 | await page.waitForSelector('div[aria-label=placeholder]') 21 | 22 | const [width, height, containerPaddingTop, text, color] = await page.evaluate(() => { 23 | const container = document.querySelector('[data-testid=virtuoso-item-list]')! 24 | const item = container.getElementsByTagName('div')[0] as HTMLElement 25 | return [item.offsetWidth, item.offsetHeight, (container as HTMLElement).style.paddingTop, item.textContent, item.style.color] 26 | }) 27 | 28 | const itemIndex = (parseInt(containerPaddingTop, 10) / 30) * 2 29 | 30 | expect(text).toBe(`Placeholder ${itemIndex}`) 31 | expect(width).toBe(300) 32 | expect(height).toBe(30) 33 | expect(color).toBe('red') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/grid.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with hundred items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'grid') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('renders 16 items', async ({ page }) => { 13 | const itemCount = await page.evaluate(() => { 14 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]') 15 | return listContainer!.childElementCount 16 | }) 17 | expect(itemCount).toBe(16) 18 | }) 19 | 20 | test('fills in the scroller', async ({ page }) => { 21 | await page.waitForTimeout(100) 22 | const scrollHeight = await page.evaluate(() => { 23 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]') 24 | return scroller!.scrollHeight 25 | }) 26 | expect(scrollHeight).toBe(2000) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/grouped-topmost-item.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('jagged grouped list', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'grouped-topmost-item') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('puts the specified item below the group', async ({ page }) => { 13 | // we pick the second item, the first should remain under the group header 14 | const stickyItemIndex = await page.evaluate(() => { 15 | const stickyItem = document.querySelector('[data-testid=virtuoso-item-list] > div:nth-child(2)') as HTMLElement 16 | return stickyItem.dataset.itemIndex 17 | }) 18 | 19 | expect(stickyItemIndex).toBe('10') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/hello.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with hundred items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'hello') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('renders only 10 items', async ({ page }) => { 13 | const itemCount = await page.evaluate(() => { 14 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]')! 15 | return listContainer.childElementCount 16 | }) 17 | expect(itemCount).toBe(10) 18 | }) 19 | 20 | test('fills in the scroller', async ({ page }) => { 21 | const scrollHeight = await page.evaluate(() => { 22 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 23 | return scroller.scrollHeight 24 | }) 25 | expect(scrollHeight).toBe(100 * 30) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/initial-scroll-top.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('initial scroll top', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'initial-scroll-top') 9 | }) 10 | 11 | test('starts from 50px', async ({ page }) => { 12 | await page.waitForTimeout(100) 13 | const scrollTop = await page.evaluate(() => { 14 | const scroller = document.querySelectorAll('[data-testid=virtuoso-scroller]')[0] 15 | return scroller.scrollTop 16 | }) 17 | 18 | expect(scrollTop).toBe(50) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/long-last-item.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with a long last item', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'long-last-item') 9 | await page.waitForTimeout(300) 10 | }) 11 | 12 | test('starts from the last item', async ({ page }) => { 13 | const paddingTop: string = await page.evaluate(() => { 14 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]')! 15 | return (listContainer as HTMLElement).style.paddingTop 16 | }) 17 | expect(paddingTop).toBe('7200px') 18 | }) 19 | 20 | test('compensates on upwards scrolling correctly', async ({ page }) => { 21 | await page.evaluate(() => { 22 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 23 | scroller.scrollBy({ top: -2 }) 24 | }) 25 | 26 | await page.waitForTimeout(200) 27 | 28 | const scrollTop = await page.evaluate(() => { 29 | return document.querySelector('[data-testid=virtuoso-scroller]')!.scrollTop 30 | }) 31 | 32 | // items are 800 and 100px tall. 33 | // scrolling up by 2px reveals an unexpectedly short item, so it should compensate 34 | expect(scrollTop).toBe(7200 - 2 - (800 - 100)) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/prepend-items.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with prependable items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'prepend-items') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | async function getScrollTop(page: Page) { 13 | await page.waitForTimeout(100) 14 | return page.locator('data-testid=virtuoso-scroller').evaluate((el) => el.scrollTop) 15 | } 16 | 17 | test('keeps the location at where it should be (2 items)', async ({ page }) => { 18 | expect(await getScrollTop(page)).toBe(0) 19 | 20 | await page.locator('data-testid=prepend-2').click() 21 | 22 | expect(await getScrollTop(page)).toBe(2 * 55) 23 | 24 | await page.locator('data-testid=prepend-2').click() 25 | 26 | expect(await getScrollTop(page)).toBe(4 * 55) 27 | }) 28 | 29 | test('keeps the location at where it should be (200 items)', async ({ page }) => { 30 | expect(await getScrollTop(page)).toBe(0) 31 | 32 | await page.locator('data-testid=prepend-200').click() 33 | 34 | expect(await getScrollTop(page)).toBe(200 * 55) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/reverse-taller-than-viewport.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | const DEFAULT_ITEM_HEIGHT = 35 7 | const OUTLIER = 400 8 | const ITEM_COUNT = 100 9 | const VIEWPORT_HEIGHT = 300 10 | const INITIAL_SCROLL_TOP = DEFAULT_ITEM_HEIGHT * ITEM_COUNT - VIEWPORT_HEIGHT 11 | const SCROLL_DELTA = -50 12 | test.describe('reverse taller than viewport', () => { 13 | test.beforeEach(async ({ baseURL, page }) => { 14 | await navigateToExample(page, baseURL, 'reverse-taller-than-viewport') 15 | await page.waitForTimeout(200) 16 | }) 17 | 18 | test('compensates for the tall 90th item', async ({ page }) => { 19 | const scroller = page.locator('[data-testid=virtuoso-scroller]') 20 | await page.waitForTimeout(300) 21 | await scroller.evaluate((el) => { 22 | el.scrollBy(0, -50) 23 | }) 24 | await page.waitForTimeout(300) 25 | const scrollTop = await scroller.evaluate((el) => el.scrollTop) 26 | 27 | expect(scrollTop).toBe(INITIAL_SCROLL_TOP + SCROLL_DELTA + OUTLIER - DEFAULT_ITEM_HEIGHT) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/scroll-seek-placeholder.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with scroll seek placeholders', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'scroll-seek-placeholder') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('renders placeholders when scrolled', async ({ page }) => { 13 | await page.evaluate(() => { 14 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]') as HTMLElement 15 | setInterval(() => { 16 | scroller.scrollBy({ top: 30 }) 17 | }, 10) 18 | }) 19 | 20 | await page.waitForSelector('div[aria-label=placeholder]') 21 | 22 | const color = await page.evaluate(() => { 23 | const placeholderItem = document.querySelector('[data-testid=virtuoso-item-list] > div') as HTMLElement 24 | return placeholderItem.style.color 25 | }) 26 | 27 | expect(color).toBe('red') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/table.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('window table with header', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'window-table') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('renders correct total height', async ({ page }) => { 13 | const height = await page.locator('[data-virtuoso-scroller]').evaluate((el) => el.style.height) 14 | expect(height).toBe('3254px') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/test-case-446.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with a long last item', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'test-case-446') 9 | await page.waitForTimeout(300) 10 | }) 11 | 12 | // the float height was causing a load of item 9 13 | test('starts from item with index 10', async ({ page }) => { 14 | const firstItemIndex = await page.evaluate(() => { 15 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]')! 16 | return (listContainer as HTMLElement).firstElementChild!.getAttribute('data-item-index') 17 | }) 18 | expect(firstItemIndex).toBe('10') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/toggle.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('list with prependable items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'toggle') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | async function getScrollTop(page: Page) { 13 | await page.waitForTimeout(100) 14 | return page.locator('data-testid=virtuoso-scroller').evaluate((el) => Math.round(el.scrollTop)) 15 | } 16 | 17 | test('keeps the location at where it should be (toggle)', async ({ page }) => { 18 | const iniitalScrollTop = await getScrollTop(page) 19 | await page.locator('data-testid=toggle-last-two').click() 20 | expect(await getScrollTop(page)).toBe(iniitalScrollTop + 100) 21 | await page.locator('data-testid=toggle-last-two').click() 22 | expect(await getScrollTop(page)).toBe(iniitalScrollTop) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/top-items.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | //@ts-expect-error - type module and playwright 4 | import { navigateToExample } from './utils.ts' 5 | 6 | test.describe('jagged list with 2 top items', () => { 7 | test.beforeEach(async ({ baseURL, page }) => { 8 | await navigateToExample(page, baseURL, 'top-items') 9 | await page.waitForTimeout(100) 10 | }) 11 | 12 | test('stays at top at start', async ({ page }) => { 13 | const scrollTop = await page.evaluate(() => { 14 | const listContainer = document.querySelector('[data-testid=virtuoso-scroller]')! 15 | return listContainer.scrollTop 16 | }) 17 | 18 | expect(scrollTop).toBe(0) 19 | 20 | const paddingTop = await page.evaluate(() => { 21 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]') as HTMLElement 22 | return listContainer.style.paddingTop 23 | }) 24 | 25 | expect(paddingTop).toBe('70px') 26 | }) 27 | 28 | test('renders correct amount of items', async ({ page }) => { 29 | const childElementCount = await page.evaluate(() => { 30 | const listContainer = document.querySelector('[data-testid=virtuoso-item-list]') 31 | return listContainer?.childElementCount || 0 32 | }) 33 | expect(childElementCount).toBe(9) 34 | }) 35 | 36 | test('renders the full list correctly', async ({ page }) => { 37 | await page.evaluate(() => { 38 | const scroller = document.querySelector('[data-testid=virtuoso-scroller]')! 39 | scroller.scrollTo({ top: 2000 }) 40 | }) 41 | 42 | await page.waitForTimeout(100) 43 | 44 | const firstChildIndex: string = await page.evaluate(() => { 45 | const firstChild = document.querySelector('[data-testid=virtuoso-item-list] > div') as HTMLElement 46 | return firstChild.dataset.index! 47 | }) 48 | 49 | expect(firstChildIndex).toBe('85') 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/react-virtuoso/e2e/utils.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test' 2 | 3 | export async function navigateToExample(page: Page, baseURL: string | undefined, exampleName: string) { 4 | await page.goto(`${baseURL}/?story=${exampleName}--example`) 5 | return page.waitForSelector('[data-storyloaded]') 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/align-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { TableVirtuoso, Virtuoso } from '../src' 4 | 5 | export function Example() { 6 | const [total, setTotal] = useState(10) 7 | return ( 8 |
9 |
header
, 13 | }} 14 | computeItemKey={(key) => `item-${key}`} 15 | followOutput={'smooth'} 16 | itemContent={(index) =>
Item {index}
} 17 | style={{ flex: 1, height: '100%' }} 18 | totalCount={total} 19 | /> 20 |
21 | 28 |
29 |
30 | ) 31 | } 32 | 33 | export function TableExample() { 34 | return ( 35 | ( 39 | <> 40 |
41 | 42 | 43 | 44 | )} 45 | style={{ border: '1px solid red', height: 400 }} 46 | /> 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/benchmark.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | const CONTENT = { 4 | 0: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec lorem eu turpis maximus rutrum. Nullam pellentesque elementum iaculis. Phasellus interdum ultricies sodales. Morbi vehicula aliquet ligula, malesuada tempus nunc. Integer viverra nunc ac augue luctus pretium. ', 5 | 1: 'Morbi et suscipit nulla. Suspendisse bibendum et ligula at ullamcorper. Praesent vehicula ut velit a commodo. In sed felis sodales, efficitur sem non, sollicitudin lorem. Fusce tempus risus lacus, a finibus ligula volutpat in. Nunc tempus nulla ut enim imperdiet, ac volutpat sem tempor. Proin hendrerit fringilla lacinia. Vivamus dignissim ultricies congue. Duis purus dui, pharetra in enim vitae, dictum accumsan nisl. Sed ornare justo eu varius facilisis.', 6 | 2: 'Cras vel augue at lorem congue tempus. Donec convallis leo neque, eu convallis mauris pulvinar et. ', 7 | } 8 | 9 | const itemContent = (index: number) => ( 10 |
{CONTENT[(index % 3) as 0 | 1 | 2]}
11 | ) 12 | 13 | export function Example() { 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/collapsible-long-item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | const Expanded = React.createContext([ 6 | false, 7 | (_val: boolean) => { 8 | void _val 9 | }, 10 | ] as const) 11 | 12 | const Item = ({ index }: { index: number }) => { 13 | const [expanded, setExpanded] = React.useContext(Expanded) 14 | 15 | return ( 16 |
17 |
Item {index}
18 | 25 |
26 | ) 27 | } 28 | 29 | const ExpandedProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 30 | const [expanded, setExpanded] = React.useState(false) 31 | return {children} 32 | } 33 | 34 | export function Example() { 35 | return ( 36 | 37 | } 41 | style={{ height: 600 }} 42 | totalCount={100} 43 | /> 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/context.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Components, Virtuoso } from '../src' 4 | 5 | const components: Components = { 6 | Header: ({ context }) =>
Header - {JSON.stringify(context)}
, 7 | } 8 | export function Example() { 9 | const [context, setContext] = React.useState({ key: 'value' }) 10 | return ( 11 | <> 12 | 19 | `item-${key.toString()}`} 22 | context={context} 23 | initialItemCount={30} 24 | itemContent={(index, _, { key }) => ( 25 |
26 | Item {index} - {key} 27 |
28 | )} 29 | style={{ height: 300 }} 30 | totalCount={100} 31 | /> 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/custom-components.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { GroupedVirtuoso } from '../src' 4 | 5 | export function Example() { 6 | return ( 7 |
Footer
, 10 | Group: ({ children, ...props }) => ( 11 |
12 | {children} 13 |
14 | ), 15 | Item: ({ children, ...props }) => ( 16 |
17 | {children} 18 |
19 | ), 20 | List: React.forwardRef(({ children, style }, ref) => ( 21 | 22 | {children} 23 | 24 | )), 25 | }} 26 | groupCounts={[10, 10, 10, 10, 10]} 27 | itemContent={(index) =>
Item {index}
} 28 | style={{ height: 300 }} 29 | topItemCount={2} 30 | /> 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/custom-scroller.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | const FancyScroller = React.forwardRef(({ children, ...props }: { children?: React.ReactNode }, ref: React.Ref) => { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
12 | ) 13 | }) 14 | 15 | export function Example() { 16 | return ( 17 | `item-${key}`} 22 | itemContent={(index) =>
Item {index}
} 23 | style={{ height: 300 }} 24 | totalCount={100} 25 | /> 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/data.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | function generateItems(length: number) { 6 | return Array.from({ length }, (_, index) => `My Item ${index}`) 7 | } 8 | 9 | const itemContent = (_: number, data: string) => { 10 | return
{data}
11 | } 12 | export function Example() { 13 | const [data, setData] = useState(() => generateItems(100)) 14 | 15 | return ( 16 |
17 | 26 | 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/decrease-items.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | const itemContent = (index: number) =>
Item {index}
6 | const style = { height: 300 } 7 | export function Example() { 8 | const [count, setCount] = useState(100) 9 | return ( 10 |
11 | 18 | 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/default-item-height.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 | `item-${key}`} 7 | defaultItemHeight={30} 8 | itemContent={(index) =>
Item {index}
} 9 | style={{ height: 300 }} 10 | totalCount={100} 11 | /> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/empty-placeholder.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | export function Example() { 6 | const [totalCount, setTotalCount] = useState(1000) 7 | return ( 8 |
9 | 16 | { 19 | console.log('empty placeholder rendered') 20 | return
Nothing to See here!
21 | }, 22 | }} 23 | computeItemKey={(key) => `item-${key}`} 24 | itemContent={(index) =>
Item {index}
} 25 | style={{ height: 300 }} 26 | totalCount={totalCount} 27 | /> 28 |

Empty placeholder should not be flashed in default rendering. check the console for logs.

29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/end-reached.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | function generateItems(length: number, iter: number) { 6 | return Array.from({ length }, (_, index) => `My Item ${index}, gen: ${iter}`) 7 | } 8 | 9 | const itemContent = (_: number, data: string) => { 10 | return
{data}
11 | } 12 | 13 | export function Example() { 14 | const [data, setData] = useState(() => generateItems(100, 1)) 15 | const [iter, setIter] = useState(1) 16 | 17 | return ( 18 |
19 | 29 | 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/endless-scrolling.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | function generateItems(length: number, startIndex: number) { 6 | return Array.from({ length }, (_, index) => `My Item ${index + startIndex}, gen: ${startIndex}`) 7 | } 8 | 9 | const itemContent = (_: number, data: string) => { 10 | return
{data}
11 | } 12 | 13 | export function Example() { 14 | const [items, setItems] = useState(() => []) 15 | 16 | const loadMore = useCallback(() => { 17 | return setTimeout(() => { 18 | setItems((items) => [...items, ...generateItems(100, items.length)]) 19 | }, 0) 20 | }, [setItems]) 21 | 22 | useEffect(() => { 23 | const timeout = loadMore() 24 | return () => { 25 | clearTimeout(timeout) 26 | } 27 | }, [loadMore]) 28 | 29 | return ( 30 |
31 | {items.length ? ( 32 | 33 | ) : ( 34 |
Loading
35 | )} 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/expandable-cards.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | const Row = (props: any) => { 6 | const { expanded, rowIndex, setExpanded } = props 7 | const [ex, setEx] = useState(expanded) 8 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 9 | const color = Math.floor(Math.abs(Math.sin(rowIndex) * 16777215) % 16777215).toString(16) 10 | return ( 11 |
12 |
{ 14 | setExpanded(!expanded) 15 | setEx(!ex) 16 | }} 17 | style={{ 18 | background: 'grey', 19 | border: '4px solid black', 20 | padding: '40px 0px', 21 | }} 22 | > 23 | This is row #{rowIndex} rendered at {Date.now()} 24 |
25 |
31 |
32 |
33 |
34 | ) 35 | } 36 | type IExpanded = Record 37 | export function Example() { 38 | const [expanded, setExpanded] = useState({}) 39 | 40 | const itemContent = (rowIndex: number) => ( 41 | { 45 | setExpanded((old) => Object.assign(old, { [rowIndex]: expanded })) 46 | }} 47 | /> 48 | ) 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/follow-output-async-expanded.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useCallback, useState } from 'react' 3 | 4 | import { Virtuoso, VirtuosoHandle } from '../src' 5 | 6 | const Image = ({ index }: { index: number }) => { 7 | const ref = React.useRef(null) 8 | React.useEffect(() => { 9 | if (index % 3 === 1) { 10 | setTimeout( 11 | () => { 12 | ref.current!.style.height = '200px' 13 | ref.current!.style.background = 'red' 14 | ref.current!.dispatchEvent(new Event('customLoad', { bubbles: true })) 15 | }, 16 | Math.random() * 100 + 200 17 | ) 18 | } 19 | }) 20 | return ( 21 |
22 | Item {index} 23 |
24 | ) 25 | } 26 | export function Example() { 27 | const [count] = useState(100) 28 | const ref = React.useRef(null) 29 | const virtuosoRef = React.useRef(null) 30 | const itemContent = useCallback((index: number) => { 31 | return 32 | }, []) 33 | 34 | React.useEffect(() => { 35 | ref.current!.addEventListener('customLoad', () => { 36 | virtuosoRef.current?.autoscrollToBottom() 37 | }) 38 | }, []) 39 | 40 | return ( 41 |
42 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/follow-output-loading-image.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useCallback, useState } from 'react' 3 | 4 | import { Virtuoso, VirtuosoHandle } from '../src' 5 | 6 | const Image = ({ index }: { index: number }) => { 7 | const ref = React.useRef(null) 8 | React.useEffect(() => { 9 | if (index > 99) { 10 | setTimeout(() => { 11 | ref.current!.style.height = '300px' 12 | ref.current!.dispatchEvent(new Event('customLoad', { bubbles: true })) 13 | }, 500) 14 | } 15 | }) 16 | return ( 17 |
18 | Item {index} 19 |
20 | ) 21 | } 22 | export function Example() { 23 | const [count, setCount] = useState(100) 24 | const ref = React.useRef(null) 25 | const virtuosoRef = React.useRef(null) 26 | const itemContent = useCallback((index: number) => { 27 | return 28 | }, []) 29 | 30 | React.useEffect(() => { 31 | ref.current!.addEventListener('customLoad', () => { 32 | virtuosoRef.current?.autoscrollToBottom() 33 | }) 34 | }, []) 35 | 36 | return ( 37 |
38 |
39 | {' '} 47 | |{' '} 48 |
49 | 57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/grid-data.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { VirtuosoGrid } from '../src' 4 | 5 | function generateItems(length: number) { 6 | return Array.from({ length }, (_, index) => `My Item ${index}`) 7 | } 8 | 9 | const itemContent = (_: number, data: string) => { 10 | return
{data}
11 | } 12 | export function Example() { 13 | const [data, setData] = useState(() => generateItems(100)) 14 | 15 | return ( 16 |
17 | 26 | 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/grid-header-footer.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | 3 | import { GridComponents, VirtuosoGrid } from '../src' 4 | 5 | const ItemContainer = styled.div` 6 | box-sizing: border-box; 7 | padding: 5px; 8 | width: 25%; 9 | background: #f5f5f5; 10 | display: flex; 11 | flex: none; 12 | align-content: stretch; 13 | /* 14 | @media (max-width: 1024px) { 15 | width: 33%; 16 | } 17 | 18 | @media (max-width: 768px) { 19 | width: 50%; 20 | } 21 | 22 | @media (max-width: 480px) { 23 | width: 100%; 24 | } 25 | */ 26 | ` 27 | 28 | const ItemWrapper = styled.div` 29 | flex: 1; 30 | text-align: center; 31 | height: 30px; 32 | padding: 20px; 33 | background: white; 34 | } 35 | ` 36 | 37 | const ListContainer = styled.div` 38 | display: flex; 39 | flex-wrap: wrap; 40 | ` as GridComponents['List'] 41 | 42 | export function Example() { 43 | return ( 44 |
Footer
, 47 | Header: () =>
Header
, 48 | Item: ItemContainer, 49 | List: ListContainer, 50 | ScrollSeekPlaceholder: () => ( 51 | 52 | Placeholder 53 | 54 | ), 55 | }} 56 | itemContent={(index) => Item {index}} 57 | overscan={150} 58 | style={{ height: 600 }} 59 | totalCount={100} 60 | /> 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/grid-infinite-data.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { VirtuosoGrid } from '../src' 4 | 5 | function generateItems(length: number) { 6 | return Array.from({ length }, (_, index) => `My Item ${index}`) 7 | } 8 | 9 | const itemContent = (_: number, data: string) => { 10 | return
{data}
11 | } 12 | export function Example() { 13 | const [data, setData] = useState(() => generateItems(5)) 14 | 15 | const loadMore = () => { 16 | setTimeout(() => { 17 | setData((prevData) => { 18 | return generateItems(prevData.length + 5) 19 | }) 20 | }, 100) 21 | } 22 | 23 | return ( 24 |
29 | 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/grid-initial-item-count-initial-topmost-item-index.tsx: -------------------------------------------------------------------------------- 1 | import { VirtuosoGrid } from '../src' 2 | 3 | export function Example() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/grid-responsive-columns.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | 3 | import { GridComponents, VirtuosoGrid } from '../src' 4 | 5 | const ItemContainer = styled.div` 6 | width: 100px; 7 | height: 100px; 8 | display: flex; 9 | ` 10 | 11 | const ItemWrapper = styled.div` 12 | flex: 1; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | font-size: 80%; 17 | border: 1px solid #000; 18 | white-space: nowrap; 19 | ` 20 | 21 | const ListContainer = styled.div` 22 | display: grid; 23 | grid-gap: 10px; 24 | grid-template-columns: repeat(auto-fill, 100px); 25 | grid-template-rows: repeat(auto-fill, 100px); 26 | justify-content: space-evenly; 27 | margin: 10px; 28 | ` as GridComponents['List'] 29 | 30 | export function Example() { 31 | return ( 32 | ( 37 | 38 | {'--'} 39 | 40 | ), 41 | }} 42 | itemContent={(index) => Item {index}} 43 | overscan={200} 44 | scrollSeekConfiguration={{ 45 | enter: (velocity) => Math.abs(velocity) > 200, 46 | exit: (velocity) => Math.abs(velocity) < 30, 47 | }} 48 | style={{ height: 340 }} 49 | totalCount={10000} 50 | /> 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/grid-scroll-seek-placeholder.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | 3 | import { GridComponents, VirtuosoGrid } from '../src' 4 | 5 | const ItemContainer = styled.div` 6 | box-sizing: border-box; 7 | padding: 5px; 8 | width: 50%; 9 | background: #f5f5f5; 10 | display: flex; 11 | flex: none; 12 | align-content: stretch; 13 | height: 30px; 14 | ` 15 | 16 | const ListContainer = styled.div` 17 | display: flex; 18 | flex-wrap: wrap; 19 | ` as GridComponents['List'] 20 | 21 | export function Example() { 22 | return ( 23 | ( 28 |
29 | Placeholder {index} 30 |
31 | ), 32 | }} 33 | computeItemKey={(key) => `item-${key}`} 34 | itemContent={(index) =>
Item {index}
} 35 | scrollSeekConfiguration={{ 36 | enter: (velocity) => Math.abs(velocity) > 200, 37 | exit: (velocity) => Math.abs(velocity) < 30, 38 | }} 39 | style={{ height: 300, width: 600 }} 40 | totalCount={10000} 41 | /> 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/group-scroll-into-view.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { GroupedVirtuoso, GroupedVirtuosoHandle } from '../src' 4 | 5 | export function Example() { 6 | const virutoso = React.useRef(null) 7 | const g = React.useRef(0) 8 | const groupCounts = React.useMemo(() => { 9 | const result = Array.from({ length: 20 }).fill(3) as number[] 10 | result.splice(13, 0, 0) 11 | return result 12 | }, []) 13 | return ( 14 |
15 | 23 | 31 |
Group {index}
} 33 | groupCounts={groupCounts} 34 | itemContent={(index) =>
Item {index}
} 35 | ref={virutoso} 36 | style={{ height: '300px' }} 37 | /> 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/group-scroll-seek-placeholder.tsx: -------------------------------------------------------------------------------- 1 | import { GroupedVirtuoso } from '../src/' 2 | 3 | export function Example() { 4 | return ( 5 | <> 6 |

Scroll fast, groups should be green placeholders

7 | ( 10 |
Placeholder {index}
11 | ), 12 | }} 13 | computeItemKey={(key) => `item-${key}`} 14 | groupContent={(index) =>
Group {index}
} 15 | groupCounts={Array.from({ length: 20 }).fill(20) as number[]} 16 | itemContent={(index) =>
Item {index}
} 17 | scrollSeekConfiguration={{ 18 | enter: (velocity) => Math.abs(velocity) > 200, 19 | exit: (velocity) => Math.abs(velocity) < 30, 20 | }} 21 | style={{ height: 800 }} 22 | /> 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/grouped-topmost-item.tsx: -------------------------------------------------------------------------------- 1 | import { GroupedVirtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 |
Group {index}
} 7 | groupCounts={Array.from({ length: 20 }).fill(3) as number[]} 8 | initialTopMostItemIndex={10} 9 | itemContent={(index) =>
Item {index}
} 10 | style={{ height: '300px' }} 11 | /> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/header-footer.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 |
Footer
, 8 | Header: () =>
Header
, 9 | }} 10 | itemContent={(index) =>
Item {index}
} 11 | overscan={150} 12 | style={{ height: 600 }} 13 | topItemCount={1} 14 | totalCount={100} 15 | /> 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/hello.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 | `item-${key.toString()}`} 7 | initialItemCount={30} 8 | itemContent={(index) =>
Item {index}
} 9 | style={{ height: 300 }} 10 | totalCount={100} 11 | /> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/horizontal.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 |
6 | `item-${key.toString()}`} 8 | horizontalDirection 9 | itemContent={(index) =>
Item {index}
} 10 | style={{ height: '100%' }} 11 | totalCount={100} 12 | /> 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/initial-from-zero.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | export function Example() { 6 | const [items, setItems] = React.useState(Array.from({ length: 0 }).map(() => 'item')) 7 | const initialTopMostItemIndex = Math.max(0, items.length - 1) 8 | // set the initialTopMostItemIndex to 999 to have the list start at the bottom 9 | return ( 10 |
11 |

virtualized

12 |
initialTopMostItemIndex = {initialTopMostItemIndex}
13 | ( 17 |
18 | {item} {index} 19 |
20 | )} 21 | style={{ border: 'solid thin gray', height: '50px', width: '350px' }} 22 | /> 23 | 24 | 31 | 32 |

non virtualized

33 |
34 | {items.map((item, index) => ( 35 |
36 | {item} {index} 37 |
38 | ))} 39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/initial-scroll-top.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src/' 2 | 3 | export function Example() { 4 | return ( 5 | `item-${key}`} 7 | initialScrollTop={50} 8 | itemContent={(index) =>
Item {index}
} 9 | style={{ height: 300 }} 10 | totalCount={100} 11 | /> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/iti-multiple.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | const itemContent = (index: number) =>
Item {index}
4 | 5 | export function App() { 6 | const data = Array(50) 7 | .fill(undefined) 8 | .map((_, i) => i) 9 | 10 | return ( 11 |
12 |
13 | {Array(20) 14 | .fill(undefined) 15 | .map((_, i) => ( 16 |
{i}
} 21 | key={i} 22 | style={{ 23 | border: '2px black solid', 24 | flex: 1, 25 | height: 400, 26 | }} 27 | /> 28 | ))} 29 |
30 |
31 | ) 32 | } 33 | 34 | export function Example() { 35 | return ( 36 | <> 37 |
38 | {Array.from({ length: 30 }).map((_, key) => { 39 | return ( 40 | 47 | ) 48 | })} 49 |
50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/long-last-item.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src/' 2 | 3 | export function Example() { 4 | return ( 5 | `item-${key}`} 7 | initialTopMostItemIndex={9} 8 | itemContent={(index) =>
Group {index}
} 9 | style={{ height: 500 }} 10 | totalCount={10} 11 | /> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/prepend-items.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useState } from 'react' 3 | 4 | import { Virtuoso } from '../src' 5 | 6 | const Item = ({ index }: { index: number }) => { 7 | React.useEffect(() => { 8 | return () => { 9 | console.log(`unmounting ${index}`) 10 | } 11 | }, [index]) 12 | 13 | return
Item {index}
14 | } 15 | const itemContent = (index: number) => 16 | 17 | const style = { height: 300 } 18 | export function Example() { 19 | const [count, setCount] = useState(100) 20 | const [firstItemIndex, setFirstItemIndex] = useState(2000) 21 | const prepend = React.useCallback( 22 | (count: number) => () => { 23 | setCount((val) => val + count) 24 | setFirstItemIndex((val) => val - count) 25 | }, 26 | [] 27 | ) 28 | return ( 29 |
30 | 33 | 36 | 39 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/prepend-last-tall-item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | export function Example() { 6 | const [fii] = React.useState(1000) 7 | return ( 8 | 16 | ) 17 | } 18 | 19 | function itemContent(index: number) { 20 | const height = index === 1099 ? 120 : 30 21 | const backgroundColor = index === 1099 ? 'red' : 'transparent' 22 | return
Item {index}
23 | } 24 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/remove-items.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | interface User { 6 | name: string 7 | } 8 | 9 | const users: User[] = [ 10 | { 11 | name: 'Sheila Robel', 12 | }, 13 | { 14 | name: 'Brian Bernier', 15 | }, 16 | { 17 | name: 'Destiny Hegmann', 18 | }, 19 | { 20 | name: 'Demetrius Schaden', 21 | }, 22 | { 23 | name: 'Tara Smitham', 24 | }, 25 | ] 26 | 27 | export function Example() { 28 | const [list, setList] = useState(users) 29 | 30 | const remove = (user: User) => { 31 | setList((prevList) => prevList.filter((u) => u.name !== user.name)) 32 | } 33 | 34 | return ( 35 | ( 39 |
40 |

41 | {user.name} 42 | 49 |

50 |
51 | )} 52 | itemsRendered={(props) => { 53 | console.log('items rendered', props) 54 | }} 55 | style={{ height: 1000 }} 56 | /> 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/rerender.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Virtuoso, VirtuosoGrid } from '../src/' 4 | 5 | export function Example() { 6 | const [, setFoo] = React.useState(Symbol()) 7 | const [bar, setBar] = React.useState<{ name: string }[]>([]) 8 | 9 | return ( 10 | <> 11 | 18 | 25 | 26 |
27 | { 30 | if (item === undefined) { 31 | debugger 32 | } 33 | return 'foo' 34 | }} 35 | style={{ height: 300 }} 36 | /> 37 |
38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/resize-loop.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | // window.addEventListener('error', (event) => { 6 | // console.log(event) 7 | // }) 8 | 9 | const ResizingDiv: FC<{ children: ReactNode }> = ({ children }) => { 10 | const [height, setHeight] = React.useState(60) 11 | React.useEffect(() => { 12 | setHeight(() => 120) 13 | }, []) 14 | return
{children}
15 | } 16 | 17 | export function Example() { 18 | return Item {index}} style={{ height: 300 }} totalCount={100} /> 19 | } 20 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/resizing-items.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | const Item = (props: { index: number }) => { 6 | const [loaded, setLoaded] = useState(false) 7 | 8 | useEffect(() => { 9 | const to = setTimeout(() => { 10 | setLoaded(true) 11 | }, 200) 12 | 13 | return () => { 14 | clearTimeout(to) 15 | } 16 | }, []) 17 | 18 | return ( 19 |
25 | {loaded ? `Item ${props.index}` : `Loading...`} 26 |
27 | ) 28 | } 29 | 30 | export function Example() { 31 | return ( 32 |
33 | { 35 | return 36 | }} 37 | style={{ height: 300 }} 38 | totalCount={100} 39 | /> 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/reverse-taller-than-viewport.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | // @ts-expect-error I know, I know, I know 4 | globalThis.VIRTUOSO_LOG_LEVEL = 0 5 | 6 | const itemContent = (index: number) =>
Item {index}
7 | export function Example() { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/scroll-into-view-align.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Virtuoso, VirtuosoHandle } from '../src' 4 | 5 | export function Example() { 6 | const ref = React.useRef(null) 7 | 8 | return ( 9 | <> 10 | ( 12 |
Item {index}
13 | )} 14 | ref={ref} 15 | style={{ height: 300 }} 16 | totalCount={100} 17 | /> 18 |
19 | 27 | 35 | 43 |
44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/scroll-seek-placeholder.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src/' 2 | 3 | export function Example() { 4 | return ( 5 | ( 8 |
9 | Placeholder {index} 10 |
11 | ), 12 | }} 13 | computeItemKey={(key) => `item-${key}`} 14 | itemContent={(index) =>
Item {index}
} 15 | scrollSeekConfiguration={{ 16 | enter: (velocity) => Math.abs(velocity) > 200, 17 | exit: (velocity) => Math.abs(velocity) < 30, 18 | }} 19 | style={{ height: 300 }} 20 | totalCount={1000} 21 | /> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/scroll-to-index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Virtuoso, VirtuosoHandle } from '../src' 4 | 5 | export function Example() { 6 | const ref = React.useRef(null) 7 | const [visible, setVisible] = React.useState(true) 8 | return ( 9 | <> 10 |
11 | 19 | 27 | 35 | 43 | 50 |
51 |
Item {index}
} 53 | ref={ref} 54 | style={{ display: visible ? 'block' : 'none', height: 300 }} 55 | totalCount={100} 56 | /> 57 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/ssr.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 | { 7 | return { text: `Item ${index}` } 8 | })} 9 | initialItemCount={30} 10 | itemContent={(_, item) =>
{item.text}
} 11 | style={{ height: 300 }} 12 | /> 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/start-reached.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | const START_INDEX = 10000 6 | const INITIAL_ITEM_COUNT = 100 7 | 8 | export function Example() { 9 | const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX) 10 | const [items, setItems] = useState(() => Array.from({ length: INITIAL_ITEM_COUNT })) 11 | 12 | const prependItems = useCallback(() => { 13 | const itemsToPrepend = 20 14 | const nextFirstItemIndex = firstItemIndex - itemsToPrepend 15 | 16 | setTimeout(() => { 17 | setFirstItemIndex(() => nextFirstItemIndex) 18 | setItems((items) => [...Array.from({ length: itemsToPrepend }), ...items]) 19 | }, 500) 20 | 21 | return false 22 | }, [firstItemIndex, setItems]) 23 | 24 | const itemContent = useCallback((index: number) => { 25 | return ( 26 |
32 | Item {index + 1} 33 |
34 | ) 35 | }, []) 36 | 37 | return ( 38 |
39 |
Loading...
, 42 | }} 43 | data={items} 44 | firstItemIndex={firstItemIndex} 45 | initialTopMostItemIndex={INITIAL_ITEM_COUNT - 1} 46 | itemContent={itemContent} 47 | startReached={prependItems} 48 | style={{ height: '600px', width: '200px' }} 49 | /> 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/state.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { StateSnapshot, Virtuoso, VirtuosoHandle } from '../src' 4 | 5 | export function Example() { 6 | const ref = React.useRef(null) 7 | const state = React.useRef(undefined) 8 | const [key, setKey] = React.useState(0) 9 | 10 | return ( 11 |
12 | 22 | 29 | 30 | `item-${key.toString()}`} 35 | itemContent={(index) =>
Item {index}
} 36 | key={key} 37 | ref={ref} 38 | restoreStateFrom={state.current} 39 | style={{ height: 300 }} 40 | totalCount={100} 41 | /> 42 |
43 | ) 44 | } 45 | 46 | function Header() { 47 | return
Header
48 | } 49 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/tall-item-reverse-scrolling.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 | ( 8 |
9 | Item {index} 10 |
11 | {Array.from({ length: index % 2 ? 3 : 20 }, (_, i) => ( 12 |
Line {i}
13 | ))} 14 |
15 |
16 | )} 17 | style={{ height: 300 }} 18 | totalCount={100} 19 | /> 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/test-case-446.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | const itemContent = (index: number, note: { content: string }) => ( 6 |
{note.content}
7 | ) 8 | // globalThis['VIRTUOSO_LOG_LEVEL'] = 0 9 | 10 | const notes: ReturnType[] = [] 11 | function note(index: number) { 12 | return { 13 | content: `Note ${index}`, 14 | index: index + 1, 15 | } 16 | } 17 | 18 | export const getNote = (index: number) => { 19 | if (!notes[index]) { 20 | notes[index] = note(index) 21 | } 22 | 23 | return notes[index] 24 | } 25 | 26 | const generateNotes = (length: number, startIndex = 0) => { 27 | return Array.from({ length }).map((_, i) => getNote(i + startIndex)) 28 | } 29 | 30 | const START_INDEX = 10000 31 | const INITIAL_ITEM_COUNT = 20 32 | 33 | export function Example() { 34 | const [topMostItemIndex, setTopMostItemIndex] = useState(10) 35 | const [notes] = useState(() => generateNotes(INITIAL_ITEM_COUNT, START_INDEX)) 36 | 37 | return ( 38 |
39 | 46 | 53 | 54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/test-case-463.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Virtuoso } from '../src' 4 | 5 | export function Example() { 6 | const [data] = useState([{ msg: 'im a bird' }, { msg: 'im a bird 2' }]) 7 | return ( 8 |
9 | { 13 | return
{data.msg}
14 | }} 15 | /> 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/test-case-638.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | // globalThis['VIRTUOSO_LOG_LEVEL'] = 0 4 | 5 | export function Example() { 6 | return ( 7 |
Item {index}
} 11 | style={{ height: 500 }} 12 | totalCount={100} 13 | /> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/test-case-896.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { GroupedVirtuoso } from '../src' 4 | 5 | const firstGroupCountMock = 2 6 | let mock = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }] 7 | mock = [{ id: 1 }, { id: 2 }] 8 | 9 | export function Example() { 10 | const [data, setData] = React.useState(mock) 11 | 12 | const removeLastItem = () => { 13 | setData((prev) => prev.slice(0, -1)) 14 | } 15 | 16 | const addLastItem = () => { 17 | setData((prev) => [...prev, { id: data.length + 1 }]) 18 | } 19 | 20 | const groupCounts = React.useMemo(() => { 21 | if (data.length > firstGroupCountMock) { 22 | return [firstGroupCountMock, data.length - firstGroupCountMock] 23 | } 24 | return [data.length] 25 | }, [data]) 26 | 27 | return ( 28 |
29 |
Group: {index}
} 33 | // groupCounts={[data.length]} 34 | groupCounts={groupCounts} 35 | itemContent={(index, _, __, { data }) => { 36 | return
{data[index]?.id}
37 | }} 38 | style={{ border: '1px dashed #ccc', height: '300px' }} 39 | /> 40 | 41 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/toggle-display-grouped.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { GroupedVirtuoso } from '../src' 4 | 5 | export function Example() { 6 | const [visible, setVisible] = React.useState(true) 7 | return ( 8 | <> 9 | 16 |
Group {index}
} 18 | groupCounts={Array.from({ length: 20 }).fill(3) as number[]} 19 | itemContent={(index) =>
Item {index}
} 20 | style={{ display: visible ? 'block' : 'none', height: '300px' }} 21 | /> 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/top-items.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 |
Item {index}
} 7 | style={{ height: 300 }} 8 | topItemCount={3} 9 | totalCount={100} 10 | /> 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/upward-scroll.tsx: -------------------------------------------------------------------------------- 1 | import { Virtuoso } from '../src' 2 | 3 | export function Example() { 4 | return 5 | } 6 | function itemContent(index: number) { 7 | const height = index === 7 ? 120 : 30 8 | const backgroundColor = index === 7 ? 'red' : 'transparent' 9 | return
Item {index}
10 | } 11 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/window-group-scroll.tsx: -------------------------------------------------------------------------------- 1 | import { GroupedVirtuoso } from '../src' 2 | 3 | export function Example() { 4 | return ( 5 |
Group {index}
} 7 | groupCounts={Array.from({ length: 20 }).fill(3) as number[]} 8 | itemContent={(index) =>
Item {index}
} 9 | style={{ height: '300px' }} 10 | useWindowScroll 11 | /> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/examples/window-table.tsx: -------------------------------------------------------------------------------- 1 | import { TableVirtuoso } from '../src/' 2 | 3 | export function Example() { 4 | return ( 5 |
6 |

red background should match the size of the table

7 |
8 | { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | ) 18 | }, 19 | }} 20 | fixedHeaderContent={() => { 21 | return ( 22 | 23 | 26 | 29 | 30 | ) 31 | }} 32 | itemContent={(index) => { 33 | return ( 34 | <> 35 | 36 | 37 | 38 | ) 39 | }} 40 | totalCount={100} 41 | useWindowScroll 42 | /> 43 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /packages/react-virtuoso/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightTestConfig } from '@playwright/test' 2 | 3 | const config: PlaywrightTestConfig = { 4 | testDir: './e2e', 5 | webServer: { 6 | command: 'npm run ladle', 7 | port: 61000, 8 | timeout: 120 * 1000, 9 | reuseExistingServer: !process.env.CI, 10 | }, 11 | 12 | use: { 13 | launchOptions: {}, 14 | }, 15 | } 16 | export default config 17 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/alignToBottomSystem.ts: -------------------------------------------------------------------------------- 1 | import { domIOSystem } from './domIOSystem' 2 | import { totalListHeightSystem } from './totalListHeightSystem' 3 | import * as u from './urx' 4 | 5 | export const alignToBottomSystem = u.system( 6 | ([{ viewportHeight }, { totalListHeight }]) => { 7 | const alignToBottom = u.statefulStream(false) 8 | 9 | // keep this for the table component only 10 | const paddingTopAddition = u.statefulStreamFromEmitter( 11 | u.pipe( 12 | u.combineLatest(alignToBottom, viewportHeight, totalListHeight), 13 | u.filter(([enabled]) => enabled), 14 | u.map(([, viewportHeight, totalListHeight]) => { 15 | return Math.max(0, viewportHeight - totalListHeight) 16 | }), 17 | u.throttleTime(0), 18 | u.distinctUntilChanged() 19 | ), 20 | 0 21 | ) 22 | 23 | return { alignToBottom, paddingTopAddition } 24 | }, 25 | u.tup(domIOSystem, totalListHeightSystem), 26 | { singleton: true } 27 | ) 28 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/comparators.tsx: -------------------------------------------------------------------------------- 1 | import { ListRange } from './interfaces' 2 | 3 | export function rangeComparator(prev: ListRange | undefined, next: ListRange) { 4 | return !!(prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) 5 | } 6 | 7 | export function tupleComparator(prev: [T1, T2] | undefined, current: [T1, T2]) { 8 | return !!(prev && prev[0] === current[0] && prev[1] === current[1]) 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/groupedListSystem.ts: -------------------------------------------------------------------------------- 1 | import { findMaxKeyValue } from './AATree' 2 | import { domIOSystem } from './domIOSystem' 3 | import { hasGroups, sizeSystem } from './sizeSystem' 4 | import * as u from './urx' 5 | export interface GroupIndexesAndCount { 6 | groupIndices: number[] 7 | totalCount: number 8 | } 9 | 10 | export function groupCountsToIndicesAndCount(counts: number[]) { 11 | return counts.reduce( 12 | (acc, groupCount) => { 13 | acc.groupIndices.push(acc.totalCount) 14 | acc.totalCount += groupCount + 1 15 | return acc 16 | }, 17 | { 18 | groupIndices: [], 19 | totalCount: 0, 20 | } 21 | ) 22 | } 23 | 24 | export const groupedListSystem = u.system( 25 | ([{ groupIndices, sizes, totalCount }, { headerHeight, scrollTop }]) => { 26 | const groupCounts = u.stream() 27 | const topItemsIndexes = u.stream<[number]>() 28 | const groupIndicesAndCount = u.streamFromEmitter(u.pipe(groupCounts, u.map(groupCountsToIndicesAndCount))) 29 | u.connect( 30 | u.pipe( 31 | groupIndicesAndCount, 32 | u.map((value) => value.totalCount) 33 | ), 34 | totalCount 35 | ) 36 | u.connect( 37 | u.pipe( 38 | groupIndicesAndCount, 39 | u.map((value) => value.groupIndices) 40 | ), 41 | groupIndices 42 | ) 43 | 44 | u.connect( 45 | u.pipe( 46 | u.combineLatest(scrollTop, sizes, headerHeight), 47 | u.filter(([_, sizes]) => hasGroups(sizes)), 48 | u.map(([scrollTop, state, headerHeight]) => findMaxKeyValue(state.groupOffsetTree, Math.max(scrollTop - headerHeight, 0), 'v')[0]), 49 | u.distinctUntilChanged(), 50 | u.map((index) => [index]) 51 | ), 52 | topItemsIndexes 53 | ) 54 | 55 | return { groupCounts, topItemsIndexes } 56 | }, 57 | u.tup(sizeSystem, domIOSystem) 58 | ) 59 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/hooks/__mocks__/useChangedChildSizes.ts: -------------------------------------------------------------------------------- 1 | import { SizeRange } from '../../' 2 | 3 | type CallbackRefParam = HTMLElement | null 4 | 5 | export default function useChangedChildSizes(callback: (sizes: SizeRange[]) => void) { 6 | const callbackRef = (elRef: CallbackRefParam) => { 7 | if (elRef) { 8 | ;(elRef as any).triggerChangedChildSizes = (sizes: SizeRange[]) => { 9 | callback(sizes) 10 | } 11 | } 12 | } 13 | 14 | return { callbackRef } 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/hooks/__mocks__/useScrollTop.ts: -------------------------------------------------------------------------------- 1 | import { ScrollContainerState } from '../../interfaces' 2 | 3 | type CallbackRefParam = HTMLElement | null 4 | 5 | export default function useSize(callback: (state: ScrollContainerState) => void) { 6 | const scrollerRef = (elRef: CallbackRefParam) => { 7 | if (elRef) { 8 | ;(elRef as any).triggerScroll = (state: ScrollContainerState) => { 9 | callback(state) 10 | } 11 | } 12 | } 13 | 14 | return { scrollByCallback: () => {}, scrollerRef, scrollToCallback: () => {} } 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/hooks/__mocks__/useSize.ts: -------------------------------------------------------------------------------- 1 | type CallbackRefParam = HTMLElement | null 2 | 3 | export default function useSize(callback: (e: HTMLElement) => void) { 4 | const callbackRef = (elRef: CallbackRefParam) => { 5 | if (elRef) { 6 | ;(elRef as any).triggerResize = (state: any) => { 7 | callback({ ...elRef, ...state }) 8 | } 9 | } 10 | } 11 | 12 | return callbackRef 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const useIsomorphicLayoutEffect = typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect 4 | 5 | export default useIsomorphicLayoutEffect 6 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/hooks/useSize.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export type CallbackRefParam = HTMLElement | null 4 | 5 | export default function useSize(callback: (e: HTMLElement) => void, enabled: boolean, skipAnimationFrame: boolean) { 6 | return useSizeWithElRef(callback, enabled, skipAnimationFrame).callbackRef 7 | } 8 | 9 | export function useSizeWithElRef(callback: (e: HTMLElement) => void, enabled: boolean, skipAnimationFrame: boolean) { 10 | const ref = React.useRef(null) 11 | 12 | let callbackRef = (_el: CallbackRefParam) => { 13 | void 0 14 | } 15 | 16 | const observer = React.useMemo(() => { 17 | if (typeof ResizeObserver !== 'undefined') { 18 | return new ResizeObserver((entries: ResizeObserverEntry[]) => { 19 | const code = () => { 20 | const element = entries[0].target as HTMLElement 21 | if (element.offsetParent !== null) { 22 | callback(element) 23 | } 24 | } 25 | skipAnimationFrame ? code() : requestAnimationFrame(code) 26 | }) 27 | } 28 | return null 29 | }, [callback, skipAnimationFrame]) 30 | 31 | callbackRef = (elRef: CallbackRefParam) => { 32 | if (elRef && enabled) { 33 | observer?.observe(elRef) 34 | ref.current = elRef 35 | } else { 36 | if (ref.current) { 37 | observer?.unobserve(ref.current) 38 | } 39 | ref.current = null 40 | } 41 | } 42 | 43 | return { callbackRef, ref } 44 | } 45 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './component-interfaces/TableVirtuoso' 2 | export * from './component-interfaces/Virtuoso' 3 | export * from './component-interfaces/VirtuosoGrid' 4 | export * from './interfaces' 5 | export { LogLevel } from './loggerSystem' 6 | export { TableVirtuoso } from './TableVirtuoso' 7 | export * from './utils/context' 8 | export { GroupedVirtuoso, Virtuoso } from './Virtuoso' 9 | export { VirtuosoGrid } from './VirtuosoGrid' 10 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/initialItemCountSystem.ts: -------------------------------------------------------------------------------- 1 | import { initialTopMostItemIndexSystem } from './initialTopMostItemIndexSystem' 2 | import { buildListStateFromItemCount, listStateSystem } from './listStateSystem' 3 | import { propsReadySystem } from './propsReadySystem' 4 | import { sizeSystem } from './sizeSystem' 5 | import * as u from './urx' 6 | 7 | export const initialItemCountSystem = u.system( 8 | ([{ data, firstItemIndex, gap, sizes }, { initialTopMostItemIndex }, { initialItemCount, listState }, { didMount }]) => { 9 | u.connect( 10 | u.pipe( 11 | didMount, 12 | u.withLatestFrom(initialItemCount), 13 | u.filter(([, count]) => count !== 0), 14 | u.withLatestFrom(initialTopMostItemIndex, sizes, firstItemIndex, gap, data), 15 | u.map(([[, count], initialTopMostItemIndexValue, sizes, firstItemIndex, gap, data = []]) => { 16 | return buildListStateFromItemCount(count, initialTopMostItemIndexValue, sizes, firstItemIndex, gap, data) 17 | }) 18 | ), 19 | listState 20 | ) 21 | return {} 22 | }, 23 | u.tup(sizeSystem, initialTopMostItemIndexSystem, listStateSystem, propsReadySystem), 24 | { singleton: true } 25 | ) 26 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/initialScrollTopSystem.ts: -------------------------------------------------------------------------------- 1 | import { domIOSystem } from './domIOSystem' 2 | import { listStateSystem } from './listStateSystem' 3 | import { propsReadySystem } from './propsReadySystem' 4 | import * as u from './urx' 5 | 6 | export const initialScrollTopSystem = u.system( 7 | ([{ didMount }, { scrollTo }, { listState }]) => { 8 | const initialScrollTop = u.statefulStream(0) 9 | 10 | u.subscribe( 11 | u.pipe( 12 | didMount, 13 | u.withLatestFrom(initialScrollTop), 14 | u.filter(([, offset]) => offset !== 0), 15 | u.map(([, offset]) => ({ top: offset })) 16 | ), 17 | (location) => { 18 | u.handleNext( 19 | u.pipe( 20 | listState, 21 | u.skip(1), 22 | u.filter((state) => state.items.length > 1) 23 | ), 24 | () => { 25 | requestAnimationFrame(() => { 26 | u.publish(scrollTo, location) 27 | }) 28 | } 29 | ) 30 | } 31 | ) 32 | 33 | return { 34 | initialScrollTop, 35 | } 36 | }, 37 | u.tup(propsReadySystem, domIOSystem, listStateSystem), 38 | { singleton: true } 39 | ) 40 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/loggerSystem.ts: -------------------------------------------------------------------------------- 1 | import * as u from './urx' 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-namespace 4 | declare namespace globalThis { 5 | let VIRTUOSO_LOG_LEVEL: LogLevel | undefined 6 | } 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-namespace 9 | declare namespace window { 10 | let VIRTUOSO_LOG_LEVEL: LogLevel | undefined 11 | } 12 | 13 | export enum LogLevel { 14 | DEBUG, 15 | INFO, 16 | WARN, 17 | ERROR, 18 | } 19 | export type Log = (label: string, message: any, level?: LogLevel) => void 20 | 21 | export interface LogMessage { 22 | label: string 23 | level: LogLevel 24 | message: any 25 | } 26 | 27 | const CONSOLE_METHOD_MAP = { 28 | [LogLevel.DEBUG]: 'debug', 29 | [LogLevel.ERROR]: 'error', 30 | [LogLevel.INFO]: 'log', 31 | [LogLevel.WARN]: 'warn', 32 | } as const 33 | 34 | const getGlobalThis = () => (typeof globalThis === 'undefined' ? window : globalThis) 35 | 36 | export const loggerSystem = u.system( 37 | () => { 38 | const logLevel = u.statefulStream(LogLevel.ERROR) 39 | const log = u.statefulStream((label: string, message: any, level: LogLevel = LogLevel.INFO) => { 40 | const currentLevel = getGlobalThis().VIRTUOSO_LOG_LEVEL ?? u.getValue(logLevel) 41 | if (level >= currentLevel) { 42 | // eslint-disable-next-line no-console 43 | console[CONSOLE_METHOD_MAP[level]]( 44 | '%creact-virtuoso: %c%s %o', 45 | 'color: #0253b3; font-weight: bold', 46 | 'color: initial', 47 | label, 48 | message 49 | ) 50 | } 51 | }) 52 | 53 | return { 54 | log, 55 | logLevel, 56 | } 57 | }, 58 | [], 59 | { singleton: true } 60 | ) 61 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/propsReadySystem.ts: -------------------------------------------------------------------------------- 1 | import { loggerSystem, LogLevel } from './loggerSystem' 2 | import * as u from './urx' 3 | 4 | export const propsReadySystem = u.system( 5 | ([{ log }]) => { 6 | const propsReady = u.statefulStream(false) 7 | 8 | const didMount = u.streamFromEmitter( 9 | u.pipe( 10 | propsReady, 11 | u.filter((ready) => ready), 12 | u.distinctUntilChanged() 13 | ) 14 | ) 15 | u.subscribe(propsReady, (value) => { 16 | value && u.getValue(log)('props updated', {}, LogLevel.DEBUG) 17 | }) 18 | 19 | return { didMount, propsReady } 20 | }, 21 | u.tup(loggerSystem), 22 | { singleton: true } 23 | ) 24 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/recalcSystem.ts: -------------------------------------------------------------------------------- 1 | import * as u from './urx' 2 | 3 | export const recalcSystem = u.system( 4 | () => { 5 | const recalcInProgress = u.statefulStream(false) 6 | return { recalcInProgress } 7 | }, 8 | [], 9 | { singleton: true } 10 | ) 11 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/scrollSeekSystem.ts: -------------------------------------------------------------------------------- 1 | import { ListRange } from './interfaces' 2 | import { ScrollSeekConfiguration } from './interfaces' 3 | import { stateFlagsSystem } from './stateFlagsSystem' 4 | import * as u from './urx' 5 | 6 | export const scrollSeekSystem = u.system( 7 | ([{ scrollVelocity }]) => { 8 | const isSeeking = u.statefulStream(false) 9 | const rangeChanged = u.stream() 10 | const scrollSeekConfiguration = u.statefulStream(false) 11 | 12 | u.connect( 13 | u.pipe( 14 | scrollVelocity, 15 | u.withLatestFrom(scrollSeekConfiguration, isSeeking, rangeChanged), 16 | u.filter(([_, config]) => !!config), 17 | u.map(([speed, config, isSeeking, range]) => { 18 | const { enter, exit } = config as ScrollSeekConfiguration 19 | if (isSeeking) { 20 | if (exit(speed, range)) { 21 | return false 22 | } 23 | } else { 24 | if (enter(speed, range)) { 25 | return true 26 | } 27 | } 28 | return isSeeking 29 | }), 30 | u.distinctUntilChanged() 31 | ), 32 | isSeeking 33 | ) 34 | 35 | u.subscribe( 36 | u.pipe(u.combineLatest(isSeeking, scrollVelocity, rangeChanged), u.withLatestFrom(scrollSeekConfiguration)), 37 | ([[isSeeking, velocity, range], config]) => { 38 | if (isSeeking && config && config.change) { 39 | config.change(velocity, range) 40 | } 41 | } 42 | ) 43 | 44 | return { isSeeking, scrollSeekConfiguration, scrollSeekRangeChanged: rangeChanged, scrollVelocity } 45 | }, 46 | u.tup(stateFlagsSystem), 47 | { singleton: true } 48 | ) 49 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/topItemCountSystem.ts: -------------------------------------------------------------------------------- 1 | import { listStateSystem } from './listStateSystem' 2 | import * as u from './urx' 3 | 4 | export const topItemCountSystem = u.system(([{ topItemsIndexes }]) => { 5 | const topItemCount = u.statefulStream(0) 6 | 7 | u.connect( 8 | u.pipe( 9 | topItemCount, 10 | u.filter((length) => length >= 0), 11 | u.map((length) => Array.from({ length }).map((_, index) => index)) 12 | ), 13 | topItemsIndexes 14 | ) 15 | return { topItemCount } 16 | }, u.tup(listStateSystem)) 17 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/totalListHeightSystem.ts: -------------------------------------------------------------------------------- 1 | import { domIOSystem } from './domIOSystem' 2 | import { listStateSystem } from './listStateSystem' 3 | import * as u from './urx' 4 | 5 | export const totalListHeightSystem = u.system( 6 | ([{ fixedFooterHeight, fixedHeaderHeight, footerHeight, headerHeight }, { listState }]) => { 7 | const totalListHeightChanged = u.stream() 8 | const totalListHeight = u.statefulStreamFromEmitter( 9 | u.pipe( 10 | u.combineLatest(footerHeight, fixedFooterHeight, headerHeight, fixedHeaderHeight, listState), 11 | u.map(([footerHeight, fixedFooterHeight, headerHeight, fixedHeaderHeight, listState]) => { 12 | return footerHeight + fixedFooterHeight + headerHeight + fixedHeaderHeight + listState.offsetBottom + listState.bottom 13 | }) 14 | ), 15 | 0 16 | ) 17 | 18 | u.connect(u.duc(totalListHeight), totalListHeightChanged) 19 | 20 | return { totalListHeight, totalListHeightChanged } 21 | }, 22 | u.tup(domIOSystem, listStateSystem), 23 | { singleton: true } 24 | ) 25 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/urx/constants.ts: -------------------------------------------------------------------------------- 1 | export const PUBLISH = 0 2 | export type PUBLISH = typeof PUBLISH 3 | 4 | export const SUBSCRIBE = 1 5 | export type SUBSCRIBE = typeof SUBSCRIBE 6 | 7 | export const RESET = 2 8 | export type RESET = typeof RESET 9 | 10 | export const VALUE = 4 11 | export type VALUE = typeof VALUE 12 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/urx/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions' 2 | export * from './pipe' 3 | export * from './streams' 4 | export * from './system' 5 | export * from './transformers' 6 | export * from './utils' 7 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/utils/approximatelyEqual.ts: -------------------------------------------------------------------------------- 1 | export function approximatelyEqual(num1: number, num2: number) { 2 | return Math.abs(num1 - num2) < 1.01 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/utils/binaryArraySearch.ts: -------------------------------------------------------------------------------- 1 | export type Comparator = (item: T, value: number) => -1 | 0 | 1 2 | 3 | export function findClosestSmallerOrEqual(items: T[], value: number, comparator: Comparator): T { 4 | return items[findIndexOfClosestSmallerOrEqual(items, value, comparator)] 5 | } 6 | 7 | export function findIndexOfClosestSmallerOrEqual(items: T[], value: number, comparator: Comparator, start = 0): number { 8 | let end = items.length - 1 9 | 10 | while (start <= end) { 11 | const index = Math.floor((start + end) / 2) 12 | const item = items[index] 13 | const match = comparator(item, value) 14 | if (match === 0) { 15 | return index 16 | } 17 | 18 | if (match === -1) { 19 | if (end - start < 2) { 20 | return index - 1 21 | } 22 | end = index - 1 23 | } else { 24 | if (end === start) { 25 | return index 26 | } 27 | start = index + 1 28 | } 29 | } 30 | 31 | throw new Error(`Failed binary finding record in array - ${items.join(',')}, searched for ${value}`) 32 | } 33 | 34 | export function findRange(items: T[], startValue: number, endValue: number, comparator: Comparator): T[] { 35 | const startIndex = findIndexOfClosestSmallerOrEqual(items, startValue, comparator) 36 | const endIndex = findIndexOfClosestSmallerOrEqual(items, endValue, comparator, startIndex) 37 | return items.slice(startIndex, endIndex + 1) 38 | } 39 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/utils/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export interface VirtuosoMockContextValue { 4 | itemHeight: number 5 | viewportHeight: number 6 | } 7 | 8 | export const VirtuosoMockContext = React.createContext(undefined) 9 | 10 | export interface VirtuosoGridMockContextValue { 11 | itemHeight: number 12 | itemWidth: number 13 | viewportHeight: number 14 | viewportWidth: number 15 | } 16 | 17 | export const VirtuosoGridMockContext = React.createContext(undefined) 18 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/utils/correctItemSize.ts: -------------------------------------------------------------------------------- 1 | export function correctItemSize(el: HTMLElement, dimension: 'height' | 'width') { 2 | return Math.round(el.getBoundingClientRect()[dimension]) 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/utils/positionStickyCssValue.ts: -------------------------------------------------------------------------------- 1 | import { simpleMemoize } from './simpleMemoize' 2 | 3 | const WEBKIT_STICKY = '-webkit-sticky' 4 | const STICKY = 'sticky' 5 | 6 | export const positionStickyCssValue = simpleMemoize(() => { 7 | if (typeof document === 'undefined') { 8 | return STICKY 9 | } 10 | const node = document.createElement('div') 11 | node.style.position = WEBKIT_STICKY 12 | return node.style.position === WEBKIT_STICKY ? WEBKIT_STICKY : STICKY 13 | }) 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/utils/simpleMemoize.ts: -------------------------------------------------------------------------------- 1 | export function simpleMemoize any>(func: T): T { 2 | let called = false 3 | let result: unknown 4 | 5 | return (() => { 6 | if (!called) { 7 | called = true 8 | result = func() 9 | } 10 | return result 11 | }) as T 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/utils/skipFrames.ts: -------------------------------------------------------------------------------- 1 | export function skipFrames(frameCount: number, callback: () => void) { 2 | if (frameCount == 0) { 3 | callback() 4 | } else { 5 | requestAnimationFrame(() => { 6 | skipFrames(frameCount - 1, callback) 7 | }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-virtuoso/src/windowScrollerSystem.ts: -------------------------------------------------------------------------------- 1 | import { domIOSystem } from './domIOSystem' 2 | import { ScrollContainerState, WindowViewportInfo } from './interfaces' 3 | import * as u from './urx' 4 | 5 | export const windowScrollerSystem = u.system(([{ scrollContainerState, scrollTo }]) => { 6 | const windowScrollContainerState = u.stream() 7 | const windowViewportRect = u.stream() 8 | const windowScrollTo = u.stream() 9 | const useWindowScroll = u.statefulStream(false) 10 | const customScrollParent = u.statefulStream(undefined) 11 | 12 | u.connect( 13 | u.pipe( 14 | u.combineLatest(windowScrollContainerState, windowViewportRect), 15 | u.map(([{ scrollHeight, scrollTop: windowScrollTop, viewportHeight }, { offsetTop }]) => { 16 | return { 17 | scrollHeight, 18 | scrollTop: Math.max(0, windowScrollTop - offsetTop), 19 | viewportHeight, 20 | } 21 | }) 22 | ), 23 | scrollContainerState 24 | ) 25 | 26 | u.connect( 27 | u.pipe( 28 | scrollTo, 29 | u.withLatestFrom(windowViewportRect), 30 | u.map(([scrollTo, { offsetTop }]) => { 31 | return { 32 | ...scrollTo, 33 | top: scrollTo.top! + offsetTop, 34 | } 35 | }) 36 | ), 37 | windowScrollTo 38 | ) 39 | 40 | return { 41 | customScrollParent, 42 | // config 43 | useWindowScroll, 44 | 45 | // input 46 | windowScrollContainerState, 47 | // signals 48 | windowScrollTo, 49 | 50 | windowViewportRect, 51 | } 52 | }, u.tup(domIOSystem)) 53 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/binaryArraySearch.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { findClosestSmallerOrEqual, findRange } from '../src/utils/binaryArraySearch' 4 | 5 | function exampleComparator(item: number, value: number) { 6 | return value === item ? 0 : value < item ? -1 : 1 7 | } 8 | 9 | describe('find closest smaller number in array', () => { 10 | it('returns the only value if 1 sized', () => { 11 | expect(findClosestSmallerOrEqual([0], 20, exampleComparator)).toBe(0) 12 | }) 13 | 14 | it('finds the closest smaller value (3 items)', () => { 15 | expect(findClosestSmallerOrEqual([0, 3, 6], 5, exampleComparator)).toBe(3) 16 | }) 17 | 18 | it('finds the closest smaller value (4 items)', () => { 19 | expect(findClosestSmallerOrEqual([0, 3, 6, 9], 5, exampleComparator)).toBe(3) 20 | }) 21 | 22 | it('finds the closest smaller value (5 items)', () => { 23 | expect(findClosestSmallerOrEqual([0, 3, 6, 9, 12], 5, exampleComparator)).toBe(3) 24 | }) 25 | 26 | it('returns the last item if value is outside of the range', () => { 27 | expect(findClosestSmallerOrEqual([0, 3, 6, 9, 12], 15, exampleComparator)).toBe(12) 28 | }) 29 | 30 | it('brute force test', () => { 31 | for (let count = 1; count < 20; count++) { 32 | for (let step = 1; step < 8; step++) { 33 | const tests = Array.from({ length: count }, (_, i) => [i, Math.floor(i / step) * step]) 34 | const data = Array.from({ length: count }, (_, index) => index * step) 35 | tests.forEach(([value, result]) => { 36 | expect(findClosestSmallerOrEqual(data, value, exampleComparator)).toBe(result) 37 | }) 38 | } 39 | } 40 | }) 41 | }) 42 | 43 | describe('find range', () => { 44 | it('returns the items within the specified ranges', () => { 45 | expect(findRange([0, 3, 6, 9, 12], 5, 11, exampleComparator)).toEqual([3, 6, 9]) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/groupedListSystem.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { groupCountsToIndicesAndCount } from '../src/groupedListSystem' 4 | 5 | describe('grouped list system', () => { 6 | describe('groupCountsToIndicesAndCount', () => { 7 | it('calculates total count and marks the group indices', () => { 8 | const counts = [10, 5, 20] 9 | const result = groupCountsToIndicesAndCount(counts) 10 | expect(result.totalCount).toEqual(10 + 5 + 20 + 3 /* 3 groups */) 11 | expect(result.groupIndices).toEqual([0, 11, 17]) 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/react-urx/ssr.test.tsx: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom' 2 | import * as React from 'react' 3 | import ReactDOMServer from 'react-dom/server' 4 | import { describe, expect, it } from 'vitest' 5 | 6 | import { systemToComponent } from '../../src/react-urx' 7 | /** 8 | * @jest-environment node 9 | */ 10 | import { connect, statefulStream, stream, system } from '../../src/urx' 11 | 12 | const simpleSystem = () => 13 | system(() => { 14 | const prop = stream() 15 | const depot = statefulStream(10) 16 | connect(prop, depot) 17 | 18 | return { depot, prop } 19 | }) 20 | 21 | const Root: React.FC<{ id: string }> = ({ id }) => { 22 | const value = useEmitterValue('depot') 23 | return
{value}
24 | } 25 | const { Component, useEmitterValue } = systemToComponent( 26 | simpleSystem(), 27 | { 28 | optional: { prop: 'prop' }, 29 | }, 30 | Root 31 | ) 32 | 33 | describe('SSR component', () => { 34 | it('sets prop values', () => { 35 | const html = ReactDOMServer.renderToString() 36 | const { document } = new JSDOM(html).window 37 | expect(document.querySelector('#root')!.textContent).toEqual('30') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./" 5 | ], 6 | "compilerOptions": { 7 | "rootDirs": [ 8 | "./", 9 | "../src" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/urx/actions.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { connect, handleNext, map, pipe, publish, statefulStream, stream, subscribe } from '../../src/urx' 4 | 5 | describe('connect', () => { 6 | it('subscribes a publisher to the emitter', () => { 7 | const a = stream() 8 | const b = stream() 9 | 10 | connect(a, b) 11 | const sub = vi.fn() 12 | subscribe(b, sub) 13 | publish(a, 4) 14 | 15 | expect(sub).toHaveBeenCalledWith(4) 16 | }) 17 | 18 | it('subscribes a publisher to the emitter (map)', () => { 19 | const a = statefulStream(0) 20 | const b = statefulStream(0) 21 | const sub = vi.fn() 22 | subscribe(b, sub) 23 | 24 | connect( 25 | pipe( 26 | a, 27 | map((val) => val * 2) 28 | ), 29 | b 30 | ) 31 | 32 | publish(a, 2) 33 | expect(sub).toHaveBeenCalledWith(4) 34 | }) 35 | 36 | it('handleNext unsub is indempotent', () => { 37 | const a = stream() 38 | 39 | const sub = vi.fn() 40 | 41 | const unsub = handleNext(a, (value) => { 42 | expect(value).toEqual('foo') 43 | }) 44 | 45 | subscribe(a, sub) 46 | 47 | publish(a, 'foo') 48 | 49 | unsub() 50 | publish(a, 'bar') 51 | expect(sub).toHaveBeenCalledWith('bar') 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/urx/combineLatest.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { combineLatest, publish, statefulStream, stream, subscribe } from '../../src/urx' 4 | 5 | describe('combine latest', () => { 6 | it('combines the provided streams', () => { 7 | const foo = statefulStream('foo') 8 | const bar = statefulStream('bar') 9 | 10 | const spy = vi.fn() 11 | subscribe(combineLatest(foo, bar), spy) 12 | 13 | expect(spy).toHaveBeenCalledWith(['foo', 'bar']) 14 | }) 15 | 16 | it('works with streams', () => { 17 | const foo = statefulStream('foo') 18 | const bar = stream() 19 | 20 | const spy = vi.fn() 21 | subscribe(combineLatest(foo, bar), spy) 22 | publish(bar, 'bar') 23 | expect(spy).toHaveBeenCalledWith(['foo', 'bar']) 24 | 25 | publish(foo, 'baz') 26 | expect(spy).toHaveBeenCalledWith(['baz', 'bar']) 27 | expect(spy).toHaveBeenCalledTimes(2) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/urx/eventHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { eventHandler, publish, reset, statefulStream, stream, subscribe } from '../../src/urx' 4 | 5 | describe('event handler', () => { 6 | it('creates a single handler subscriber for a stream', () => { 7 | const str = stream() 8 | const handler = eventHandler(str) 9 | const handle1 = vi.fn() 10 | const handle2 = vi.fn() 11 | subscribe(handler, handle1) 12 | publish(str, 10) 13 | expect(handle1).toHaveBeenCalledWith(10) 14 | subscribe(handler, handle2) 15 | publish(str, 20) 16 | expect(handle2).toHaveBeenCalledWith(20) 17 | expect(handle1).toHaveBeenCalledTimes(1) 18 | }) 19 | 20 | it('unsubscribes the handle when reset', () => { 21 | const str = stream() 22 | const handler = eventHandler(str) 23 | const handle1 = vi.fn() 24 | subscribe(handler, handle1) 25 | publish(str, 10) 26 | reset(handler) 27 | publish(str, 20) 28 | expect(handle1).toHaveBeenCalledTimes(1) 29 | }) 30 | 31 | it('unsubscribes the handle when reset', () => { 32 | const str = statefulStream(1) 33 | const handler = eventHandler(str) 34 | const handle = vi.fn() 35 | subscribe(handler, handle) 36 | subscribe(handler, handle) 37 | expect(handle).toHaveBeenCalledTimes(1) 38 | }) 39 | 40 | it('accepts nullish handle as unsubscribe', () => { 41 | const str = stream() 42 | const handler = eventHandler(str) 43 | const handle1 = vi.fn() 44 | subscribe(handler, handle1) 45 | publish(str, 10) 46 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 47 | subscribe(handler, undefined as unknown as any) 48 | publish(str, 20) 49 | expect(handle1).toHaveBeenCalledTimes(1) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/urx/merge.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { merge, publish, stream, subscribe } from '../../src/urx' 4 | 5 | describe('merge', () => { 6 | it('emits values from both emitters', () => { 7 | const stream1 = stream() 8 | const stream2 = stream() 9 | const result = merge(stream1, stream2) 10 | const sub = vi.fn() 11 | subscribe(result, sub) 12 | publish(stream1, 1) 13 | expect(sub).toHaveBeenCalledWith(1) 14 | publish(stream2, 2) 15 | expect(sub).toHaveBeenCalledWith(2) 16 | }) 17 | it('cancels subscriptions', () => { 18 | const stream1 = stream() 19 | const stream2 = stream() 20 | const result = merge(stream1, stream2) 21 | const sub = vi.fn() 22 | const unsub = subscribe(result, sub) 23 | publish(stream1, 1) 24 | expect(sub).toHaveBeenCalledWith(1) 25 | publish(stream2, 2) 26 | expect(sub).toHaveBeenCalledWith(2) 27 | unsub() 28 | publish(stream1, 3) 29 | publish(stream2, 4) 30 | expect(sub).toHaveBeenCalledTimes(2) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/urx/statefulStream.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { getValue, statefulStream, subscribe } from '../../src/urx' 4 | 5 | describe('behavior stream', () => { 6 | it('creates a next / subscribe function', () => { 7 | const foo = statefulStream(5) 8 | const callback = vi.fn() 9 | subscribe(foo, callback) 10 | expect(callback).toHaveBeenCalledWith(5) 11 | }) 12 | 13 | it('extracts the value from a stream', () => { 14 | const foo = statefulStream(5) 15 | const value = getValue(foo) 16 | expect(value).toBe(5) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/urx/stream.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { publish, stream, subscribe } from '../../src/urx' 4 | 5 | describe('stream', () => { 6 | it('creates a next / subscribe function', () => { 7 | const foo = stream() 8 | const callback = vi.fn() 9 | subscribe(foo, callback) 10 | publish(foo, 4) 11 | expect(callback).toHaveBeenCalledWith(4) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/urx/system.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { init, statefulStream, system, tup } from '../../src/urx' 4 | 5 | describe('system', () => { 6 | it('run executes the factory', () => { 7 | const a = statefulStream(0) 8 | 9 | const eng = system(() => { 10 | return { a } 11 | }) 12 | 13 | const sys = init(eng) 14 | expect(sys).toMatchObject({ a }) 15 | }) 16 | 17 | it('run initiates the dependencies', () => { 18 | const a = statefulStream(0) 19 | const b = statefulStream(0) 20 | 21 | const eng = system(() => { 22 | return { a } 23 | }) 24 | 25 | const eng2 = system(([{ a }]) => { 26 | return { a, b } 27 | }, tup(eng)) 28 | 29 | expect(init(eng2)).toMatchObject({ a, b }) 30 | }) 31 | 32 | it('singleton instantiates the system only once', () => { 33 | const system1 = system( 34 | () => { 35 | const a = statefulStream(0) 36 | return { a } 37 | }, 38 | [], 39 | { singleton: true } 40 | ) 41 | 42 | const system2 = system(([{ a }]) => { 43 | const b = statefulStream(0) 44 | return { a, b } 45 | }, tup(system1)) 46 | 47 | const system3 = system(([{ a }]) => { 48 | const c = statefulStream(0) 49 | return { a, c } 50 | }, tup(system1)) 51 | 52 | const system4 = system( 53 | ([{ a: a1, b }, { a: a2, c }]) => { 54 | expect(a1).toBe(a2) 55 | return { b, c } 56 | }, 57 | tup(system2, system3) 58 | ) 59 | 60 | init(system4) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/urx/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { call, joinProc } from '../../src/urx' 4 | 5 | describe('utils', () => { 6 | describe('call', () => { 7 | it('calls the argument', () => { 8 | const proc = vi.fn() 9 | call(proc) 10 | expect(proc).toHaveBeenCalledTimes(1) 11 | }) 12 | }) 13 | 14 | describe('joinProc', () => { 15 | it('calls all procs passed', () => { 16 | const proc1 = vi.fn() 17 | const proc2 = vi.fn() 18 | const proc = joinProc(proc1, proc2) 19 | proc() 20 | expect(proc1).toHaveBeenCalledTimes(1) 21 | expect(proc2).toHaveBeenCalledTimes(1) 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/react-virtuoso/test/windowScrollerSystem.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | 3 | import { listSystem } from '../src/listSystem' 4 | import * as u from '../src/urx' 5 | 6 | describe('window scroller system', () => { 7 | it('offsets the window scroll top with the element offset top', () => { 8 | const { scrollTop, windowScrollContainerState, windowViewportRect } = u.init(listSystem) 9 | const sub = vi.fn() 10 | u.subscribe(scrollTop, sub) 11 | u.publish(windowViewportRect, { offsetTop: 100, visibleHeight: 1000 }) 12 | u.publish(windowScrollContainerState, { scrollHeight: 1000, scrollTop: 0, viewportHeight: 400 }) 13 | expect(sub).toHaveBeenCalledWith(0) 14 | u.publish(windowScrollContainerState, { scrollHeight: 1000, scrollTop: 200, viewportHeight: 400 }) 15 | expect(sub).toHaveBeenCalledWith(100) 16 | }) 17 | it('offsets the scrollTo calls with offsetTop', () => { 18 | const { scrollTo, windowScrollTo, windowViewportRect } = u.init(listSystem) 19 | const sub = vi.fn() 20 | u.subscribe(windowScrollTo, sub) 21 | u.publish(windowViewportRect, { offsetTop: 200, visibleHeight: 1000 }) 22 | u.publish(scrollTo, { top: 300 }) 23 | expect(sub).toHaveBeenCalledWith({ top: 500 }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/react-virtuoso/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "noEmit": true, 6 | "emitDeclarationOnly": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "importHelpers": true, 10 | "isolatedModules": true, 11 | "jsx": "react-jsx", 12 | "lib": ["dom", "esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "outDir": "dist", 20 | "rootDir": "./", 21 | "skipLibCheck": true, 22 | "sourceMap": true, 23 | "strict": true, 24 | "target": "ESNext", 25 | "allowJs": true 26 | }, 27 | "include": [ 28 | "examples", 29 | "test", 30 | "e2e2", 31 | "e2e", 32 | "src", 33 | "vite.config.ts", 34 | "./eslint.config.mjs", 35 | "playwright.config.ts", 36 | ".ladle/config.mjs" 37 | ], 38 | "exclude": ["node_modules", "dist"] 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-virtuoso/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc' 2 | /// 3 | import { defineConfig } from 'vite' 4 | import dts from 'vite-plugin-dts' 5 | 6 | const ext = { 7 | cjs: 'cjs', 8 | es: 'mjs', 9 | } 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | build: { 14 | lib: { 15 | entry: ['src/index.tsx'], 16 | //@ts-expect-error not sure why 17 | fileName: (format) => `index.${ext[format]}`, 18 | formats: ['es', 'cjs'], 19 | }, 20 | minify: true, 21 | rollupOptions: { 22 | external: ['react', 'react-dom', 'react/jsx-runtime', '@virtuoso.dev/urx', '@virtuoso.dev/react-urx'], 23 | }, 24 | target: ['es2020', 'edge88', 'firefox78', 'chrome79', 'safari14'], 25 | }, 26 | plugins: [react(), dts({ rollupTypes: true })], 27 | test: { 28 | environment: 'jsdom', 29 | include: ['test/**/*.test.{ts,tsx}'], 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /packages/tooling/README.md: -------------------------------------------------------------------------------- 1 | # Virtuoso Tooling 2 | 3 | This package contains shared tooling configuration for Virtuoso projects: 4 | 5 | - ESLint configuration 6 | - TypeScript configuration 7 | 8 | ## Usage 9 | 10 | Add the package as a dev dependency: 11 | 12 | ```bash 13 | npm install --save-dev @virtuoso.dev/tooling 14 | ``` 15 | 16 | ### ESLint 17 | 18 | In your project's `eslint.config.mjs`: 19 | 20 | ```js 21 | import virtuosoEslintConfig from '@virtuoso.dev/tooling/eslint.config.mjs' 22 | 23 | export default [...virtuosoEslintConfig] 24 | ``` 25 | 26 | ### TypeScript 27 | 28 | Extend the shared TypeScript configuration in your project's `tsconfig.json`: 29 | 30 | ```json 31 | { 32 | "extends": "@virtuoso.dev/tooling/tsconfig.json", 33 | "compilerOptions": { 34 | // Additional project-specific options 35 | }, 36 | "include": ["src/**/*"] 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/tooling/eslint.config.d.mts: -------------------------------------------------------------------------------- 1 | import tseslint from 'typescript-eslint' 2 | 3 | declare const defaultExport: ReturnType 4 | export default defaultExport 5 | -------------------------------------------------------------------------------- /packages/tooling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtuoso.dev/tooling", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "files": [ 7 | "eslint.config.mjs", 8 | "tsconfig.json" 9 | ], 10 | "exports": { 11 | "./eslint.config": "./eslint.config.mjs", 12 | "./prettier.config": "./prettier.config.mjs", 13 | "./vite.config": "./vite-config.mjs", 14 | "./tsconfig.base.json": "./tsconfig.base.json" 15 | }, 16 | "dependencies": { 17 | "@eslint/js": "^9.23.0", 18 | "@eslint/markdown": "^6.3.0", 19 | "typescript-eslint": "^8.22.0", 20 | "eslint-plugin-prettier": "^5.2.3", 21 | "eslint-plugin-react": "^7.37.4", 22 | "eslint-plugin-perfectionist": "^4.7.0", 23 | "eslint-plugin-react-hooks": "^5.1.0", 24 | "prettier": "^3.4.2", 25 | "globals": "^14.0.0" 26 | }, 27 | "peerDependencies": { 28 | "typescript": ">=5.7.3", 29 | "eslint": ">=9.0.0", 30 | "prettier": ">=3.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/tooling/prettier.config.d.mts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'prettier' 2 | 3 | declare const config: Config 4 | export default config 5 | -------------------------------------------------------------------------------- /packages/tooling/prettier.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | printWidth: 140, 3 | semi: false, 4 | singleQuote: true, 5 | trailingComma: 'es5', 6 | } 7 | -------------------------------------------------------------------------------- /packages/tooling/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2023", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "allowJs": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/tooling/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.base.json", 4 | "include": ["vite.config.ts", "eslint.config.mjs", "prettier.config.mjs"] 5 | } 6 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "typecheck": { 6 | "dependsOn": [ 7 | "^build", 8 | "^typecheck" 9 | ] 10 | }, 11 | "lint": { 12 | "dependsOn": [ 13 | "^build", 14 | "^lint" 15 | ] 16 | }, 17 | "test": { 18 | "dependsOn": [ 19 | "^build", 20 | "^test" 21 | ] 22 | }, 23 | "e2e": { 24 | "dependsOn": [ 25 | "^e2e" 26 | ] 27 | }, 28 | "build": { 29 | "env": [ 30 | "PADDLE_ENVIRONMENT", 31 | "PADDLE_TOKEN", 32 | "PADDLE_STANDARD_PRICE_ID", 33 | "PADDLE_PRO_PRICE_ID" 34 | ], 35 | "dependsOn": [ 36 | "^build" 37 | ] 38 | }, 39 | "ci-setup": { 40 | "dependsOn": [ 41 | "^ci-setup" 42 | ] 43 | }, 44 | "dev": { 45 | "cache": false, 46 | "persistent": true 47 | } 48 | } 49 | } 50 | --------------------------------------------------------------------------------
NameDescriptionDescriptionDescriptionDescriptionDescription
{user.name}{user.description}{user.description}{user.description}{user.description}{user.description}
NameDescription
{user.name}{user.description}{index}{string}{string}
Empty
24 | TH 1 25 | 27 | TH meh 28 |
{index}Cell 1Cell 2