diff --git a/.babelrc b/.babelrc deleted file mode 100644 index b826552..0000000 --- a/.babelrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "presets": ["@babel/preset-typescript"], - "plugins": [ - ["@babel/plugin-proposal-class-properties", { "loose": true }], - "@babel/plugin-proposal-nullish-coalescing-operator" - ] -} diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 0000000..3668f2b --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,15 @@ +name: "ESLint" +on: [pull_request] +jobs: + eslint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@v2 + with: + version: 10 + - name: Install dependencies + run: pnpm install --frozen-lockfile --recursive + - name: Run ESLint + run: pnpm lint diff --git a/.github/workflows/pending-changes.yml b/.github/workflows/pending-changes.yml new file mode 100644 index 0000000..6fb2d40 --- /dev/null +++ b/.github/workflows/pending-changes.yml @@ -0,0 +1,16 @@ +name: "Pending changes" +on: [pull_request] +jobs: + pending-changes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@v2 + with: + version: 10 + - name: Install dependencies + run: pnpm install --frozen-lockfile --recursive + - uses: nickcharlton/diff-check@main + with: + command: pnpm run compile diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..0abbbcd --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,37 @@ +name: "Playwright Tests" +on: [pull_request] +jobs: + e2e-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@v2 + with: + version: 10 + - name: Install dependencies + run: pnpm install --frozen-lockfile --recursive + + - name: Cache Playwright browser binaries + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright # Default Linux path + key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright browsers and dependencies (on cache miss) + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm e2e:install + + - name: Build library + run: pnpm prerelease + - name: Run Playwright tests + run: pnpm dev:integrations & pnpm e2e:test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: integrations/vite/test-results/ + retention-days: 14 \ No newline at end of file diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 0000000..a0307d4 --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,15 @@ +name: "Prettier" +on: [pull_request] +jobs: + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@v2 + with: + version: 10 + - name: Install dependencies + run: pnpm install --frozen-lockfile --recursive + - name: Run Prettier + run: pnpm run prettier:ci diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml new file mode 100644 index 0000000..e32fb91 --- /dev/null +++ b/.github/workflows/typescript.yml @@ -0,0 +1,17 @@ +name: "TypeScript" +on: [pull_request] +jobs: + typescript: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@v2 + with: + version: 10 + - name: Install dependencies + run: pnpm install --frozen-lockfile --recursive + - name: Build NPM package + run: pnpm build + - name: Run TypeScript + run: pnpm tsc diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml new file mode 100644 index 0000000..abbd39f --- /dev/null +++ b/.github/workflows/vitest.yml @@ -0,0 +1,17 @@ +name: "Vitest" +on: [pull_request] +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@v2 + with: + version: 10 + - name: Install dependencies + run: pnpm install --frozen-lockfile --recursive + - name: Build NPM packages + run: pnpm run build + - name: Run tests + run: pnpm run test:ci diff --git a/.gitignore b/.gitignore index acd8726..68b9758 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,26 @@ -dist +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + node_modules +/dist +/docs +*.local +integrations/vite/test-results + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea .DS_Store -.cache -*.log -.parcel-cache -.pnp.* \ No newline at end of file +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.prettierignore b/.prettierignore index 9356898..db8bc77 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,5 @@ -dist -node_modules -.trunk \ No newline at end of file +/dist +/docs +/generated +/public +/src/routes/examples diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index f0eb61e..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 2, - "semi": true, - "singleQuote": false -} diff --git a/CHANGELOG.md b/CHANGELOG.md index ff158d3..4c3f4fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # Changelog +# 2.0.0 + +Version 2 simplifies the API and improves TypeScript support. + +## Migrating from 1.x to 2.x + +Refer to [the docs](https://react-virtualized-auto-sizer.vercel.app/) for a complete list of props and API methods. Below are some examples of migrating from version 1 to 2, but first a couple of potential questions: + +
+
Q: Why were the defaultHeight and defaultWidth props removed?
+
A: The more idiomatic way of setting default width and height is to use default parameters; see below for examples.
+
Q: Why were the disableHeight and disableWidth props removed?
+
A: These props interfered with the TypeScript inference (see issue #100). `React.memo` can be used to achieve this behavior instead; see below for examples.
+
Q: Why was the doNotBailOutOnEmptyChildren prop removed?
+
A: This component no longer bails out on empty children; this decision is left up to the child component.
+
Q: Does AutoSizer support CSS transitions/animations?
+
A: To an extent, but as with ResizeObserver, there is no event dispatched when a CSS transition is complete (see issue #99). As a potential workaround, the box property can be used to report unscaled size; see below for examples.
+
+ +### Basic usage +```tsx +// Version 1 + + {({ height, width }) => { + // ... + }} + + +// Version 2 + { + // ... + }} +/> +``` + +### Default width/height for server rendered content +```tsx +// Version 1 + + +// Version 2 + { + // ... + }} +/> +``` + +### Width only (or height only) +```tsx +// Version 1 + + +// Version 2 + + +const MemoizedChild = memo( + Child, + function arePropsEqual(oldProps, newProps) { + return oldProps.height === newProps.height; + } +); +``` + ## 1.0.26 - Changed `width` and `height` values to be based om `getBoundingClientRect` rather than `offsetWidth` and `offsetHeight` ([which are integers](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth#value) and can cause rounding/flickering problems in some cases). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c521332 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing + +Thanks for your interest in contributing to this project! + +Here are a couple of guidelines to keep in mind before opening a Pull Request: + +- Please open a GitHub issue for discussion _before_ submitting any significant changes to this API (including new features or functionality). +- Please don't submit code that has been written by code-generation tools such as Copilot or Claude. (There's nothing wrong with these tools, but I'd prefer them not be a part of this project.) + +## Local development + +To get started: +```sh +pnpm install +``` + +### Running the documentation site locally + +The documentation site is a great place to test pending changes. It runs on localhost port 3000 and can be started by running: +```sh +pnpm dev +``` + +### Running tests locally + +To run unit tests locally: +```sh +pnpm test +``` + +To run end to end tests locally: +```sh +pnpm prerelease +pnpm dev:integrations & pnpm e2e:test +``` + +### Updating assets + +Before subtmitting, also make sure to update generated docs/examples: +``` +pnpm compile +pnpm prettier +pnpm lint +``` + +> [!NOTE] +> If you forget this step, CI will remind you! \ No newline at end of file diff --git a/README.md b/README.md index 3350c7f..d0abeae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # react-virtualized-auto-sizer -Standalone version of the `AutoSizer` component from [`react-virtualized`](https://github.com/bvaughn/react-virtualized). +This package measures the available width and height of an `HTMLElement` and passes those values as props to a `Child` component. Refer to [the docs](https://react-virtualized-auto-sizer.vercel.app/examples/basic-usage) for usage examples. + +> [!NOTE] +> This package began as a fork of the `AutoSizer` component from [react-virtualized](https://github.com/bvaughn/react-virtualized), and was intended for use with earlier versions of [react-window](https://github.com/bvaughn/react-virtualized). More recent versions of `react-window` use `ResizeObserver` natively and do not require this package. ### If you like this project, 🎉 [become a sponsor](https://github.com/sponsors/bvaughn/) or ☕ [buy me a coffee](http://givebrian.coffee/) @@ -10,37 +13,102 @@ Standalone version of the `AutoSizer` component from [`react-virtualized`](https npm install --save react-virtualized-auto-sizer ``` -## Documentation - - -| Property | Type | Required? | Description | -| :------------ | :------- | :-------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| children | Function | ✓ | Function responsible for rendering children. This function should implement the following signature: `({ height?: number \| undefined, width?: number \| undefined }) => PropTypes.element` | -| className | String | | Optional custom CSS class name to attach to root `AutoSizer` element. This is an advanced property and is not typically necessary. | -| defaultHeight | Number | | Height passed to child for initial render; useful for server-side rendering. This value will be overridden with an accurate height after mounting. | -| defaultWidth | Number | | Width passed to child for initial render; useful for server-side rendering. This value will be overridden with an accurate width after mounting. | -| disableHeight | Boolean | | Fixed `height`; if specified, the child's `height` property will not be managed | -| disableWidth | Boolean | | Fixed `width`; if specified, the child's `width` property will not be managed | -| doNotBailOutOnEmptyChildren | boolean | | Optional propr that can override default behavior of not rendering children when either `width` or `height` are 0 | -| nonce | String | | Nonce of the inlined stylesheets for [Content Security Policy](https://www.w3.org/TR/2016/REC-CSP2-20161215/#script-src-the-nonce-attribute) | -| onResize | Function | | Callback to be invoked on-resize; it is passed the following named parameters: `({ height: number, width: number })`. | -| style | Object | | Optional custom inline style to attach to root `AutoSizer` element. This is an advanced property and is not typically necessary. | -| tagName | string | | Optional HTML tag name for root element; defaults to `"div"` | - -## Examples - -Some components (like those found in [`react-window`](https://github.com/bvaughn/react-window) or [`react-virtualized`](https://github.com/bvaughn/react-virtualized)) require numeric width and height parameters. The `AutoSizer` component can be useful if you want to pass percentage based dimensions. +### AutoSizer + + +Measures the available width and height of its parent `HTMLElement` and passes those values as `width` and `height` props to its `children`. + +ℹ️ This component began as a fork of the [javascript-detect-element-resize](https://www.npmjs.com/package/javascript-detect-element-resize) package. + + +#### Required props + + + + + + + + + + + + + + + + +
NameDescription
Child

Child component to be passed the available width and height values as props.

+

ℹ️ Width and height are undefined during the during the initial render (including server-rendering).

+
+ + + +#### Optional props + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
box

Corresponds to the ResizeObserver box parameter. +Sets which box model the observer will observe changes to.

+
    +
  • border-box: Size of the box border area as defined in CSS.
  • +
  • content-box: Size of the content area as defined in CSS.
  • +
  • device-pixel-content-box: The size of the content area as defined in CSS, in device pixels, before applying any CSS transforms on the element or its ancestors.
  • +
+
className

Class name to be applied to the auto-sizer HTMLElement.

+
data-testid

Test id attribute to interop with frameworks like Testing Library.

+
id

Unique id attribute to attach to root DOM element.

+
nonce

Nonce used for inline StyleSheet +in browsers/environments that do not support the ResizeObserver API.

+
onResize

Optional callback notified after a resize. +@param size New width and height of parent element

+
style

Style properties to be applied to the auto-sizer HTMLElement.

+
tagName

Optional HTML tag name for root HTMLElement; defaults to "div".

+
+ + -```jsx -import AutoSizer from "react-virtualized-auto-sizer"; - -// UI - - {({ height, width }) => { - // Use these actual sizes to calculate your percentage based sizes - }} -; -``` ## FAQs @@ -48,48 +116,38 @@ import AutoSizer from "react-virtualized-auto-sizer"; Flex containers don't prevent their children from growing and `AutoSizer` greedily grows to fill as much space as possible. Combining the two can be problematic. The simple way to fix this is to nest `AutoSizer` inside of a `block` element (like a `
`) rather than putting it as a direct child of the flex container, like so: -```jsx +```tsx
- + {/* Other children... */}
- - {({ height, width }) => ( - - )} - +
``` ### Why is `AutoSizer` passing a height of 0? -`AutoSizer` expands to _fill_ its parent but it will not _stretch_ the parent. This is done to prevent problems with flexbox layouts. If `AutoSizer` is reporting a height (or width) of 0- then it's likely that the parent element (or one of its parents) has a height of 0. +`AutoSizer` expands to _fill_ its parent but it will not _stretch_ the parent. This is done to prevent problems with Flex layouts. If `AutoSizer` is reporting a height (or width) of 0- then it's likely that the parent element (or one of its parents) has a height of 0. -The solution to this problem is often to add `height: 100%` or `flex: 1` to the parent. One easy way to test this is to add a style property (eg `background-color: red;`) to the parent to visually confirm that it is the expected size. +The solution to this problem is often to add `height: 100%` or `flex: 1` to the parent. One easy way to test this is to add a style property (eg. `background-color: red;`) to the parent to visually confirm that it is the expected size. -### Can I use `AutoSizer` to manage only width or height (not both)? +### Can I use `AutoSizer` to manage _only_ width or height (not both)? -You can use `AutoSizer` to control only one dimension of its child component using the `disableHeight` or `disableWidth` attributes. For example, a fixed-height component that should grow to fill the available width can be created like so: +No, but you can memoize your child component so that it only re-renders if width (or height) changes. +```tsx +import { memo } from "react"; -```jsx - - {({width}) => } - +const MemoizedChild = memo( + Child, + function arePropsEqual(oldProps, newProps) { + return oldProps.height === newProps.height; + } +); ``` - -### Module parsing fails because of an unexpected token? - -This package targets [ECMAScript 2015](https://262.ecma-international.org/6.0/) (ES6) and requires a build tool such as [babel-loader](https://www.npmjs.com/package/babel-loader) that is capable of parsing the ES6 `class` syntax. - ### Can this component work with a Content Security Policy? -[The specification of Content Security Policy](https://www.w3.org/TR/2016/REC-CSP2-20161215/#intro) -describes as the following: +[The specification of Content Security Policy](https://www.w3.org/TR/2016/REC-CSP2-20161215/#intro) describes as the following: > This document defines Content Security Policy, a mechanism web applications > can use to mitigate a broad class of content injection vulnerabilities, such diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6f4ebde --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,66 @@ +import js from "@eslint/js"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import { globalIgnores } from "eslint/config"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config([ + globalIgnores(["dist", "docs", "generated", "integrations/next/.next"]), + { + files: ["**/*.{ts,tsx}"], + ignores: ["**/*.example.tsx"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs["recommended-latest"], + reactRefresh.configs.vite + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + tsconfigRootDir: import.meta.dirname + } + }, + rules: { + "no-restricted-imports": [ + "error", + { + patterns: ["*/../lib/*", "node:test"] + } + ], + "no-restricted-properties": [ + "error", + { + property: "clientHeight", + message: + "Using clientHeight is restricted; prefer offsetHeight or getBoundingClientRect()" + }, + { + property: "clientWidth", + message: + "Using clientWidth is restricted; prefer offsetWidth or getBoundingClientRect()" + } + ], + "react-hooks/exhaustive-deps": [ + "error", + { + additionalHooks: "useIsomorphicLayoutEffect" + } + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true + } + ] + } + } +]); diff --git a/index.css b/index.css new file mode 100644 index 0000000..3da6c53 --- /dev/null +++ b/index.css @@ -0,0 +1,92 @@ +@import "tailwindcss"; + +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + font-size: 12px; + + @media only screen and (max-width: 600px) { + font-size: 16px; + } + + color-scheme: dark; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@keyframes background-gradient-animation { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +#root { + width: 100dvw; + height: 100dvh; + overflow: auto; + background: linear-gradient( + -45deg, + var(--color-fuchsia-500), + var(--color-violet-500), + var(--color-fuchsia-700) + ); + background-size: 400% 400%; + animation: background-gradient-animation 20s ease infinite; +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--color-slate-600) transparent; + outline: none; + transition: + color 0.25s ease, + background-color 0.25s ease, + border-color 0.25s ease, + opacity 0.25s ease, + outline-color 0.25s ease; +} + +main { + [data-link], + a { + cursor: pointer; + color: var(--color-sky-400); + &:hover { + color: var(--color-sky-300); + } + } +} + +*[data-focus] { + border: 2px solid transparent; + &:focus { + border: 2px solid var(--color-sky-300); + } +} + +*[data-focus-within] { + border: 2px solid transparent; + &:focus-within { + border: 2px solid var(--color-sky-300); + } + + &[data-focus-within="bold"] { + border-color: var(--color-sky-600); + &:focus-within { + border: 2px solid var(--color-sky-300); + } + } +} + +code { + color: rgba(255, 255, 255, 0.8); +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..f9de753 --- /dev/null +++ b/index.html @@ -0,0 +1,41 @@ + + + + react-virtualized-auto-sizer | (re)sizing helper component + + + + + + + + + + + + + + + +
+ + + + diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..7cce568 --- /dev/null +++ b/index.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./src/App.tsx"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/integrations/vite/README.md b/integrations/vite/README.md new file mode 100644 index 0000000..da98444 --- /dev/null +++ b/integrations/vite/README.md @@ -0,0 +1,54 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config({ + extends: [ + // Remove ...tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config({ + plugins: { + // Add the react-x and react-dom plugins + 'react-x': reactX, + 'react-dom': reactDom, + }, + rules: { + // other rules... + // Enable its recommended typescript rules + ...reactX.configs['recommended-typescript'].rules, + ...reactDom.configs.recommended.rules, + }, +}) +``` diff --git a/integrations/vite/eslint.config.js b/integrations/vite/eslint.config.js new file mode 100644 index 0000000..fc0144d --- /dev/null +++ b/integrations/vite/eslint.config.js @@ -0,0 +1,43 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + tsconfigRootDir: import.meta.dirname + } + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh + }, + rules: { + ...reactHooks.configs.recommended.rules, + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true + } + ], + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true } + ] + } + } +); diff --git a/integrations/vite/index.html b/integrations/vite/index.html new file mode 100644 index 0000000..abcf27a --- /dev/null +++ b/integrations/vite/index.html @@ -0,0 +1,12 @@ + + + + + + [Vite] react-resizable-panels integration + + +
+ + + diff --git a/integrations/vite/package.json b/integrations/vite/package.json new file mode 100644 index 0000000..1144cb9 --- /dev/null +++ b/integrations/vite/package.json @@ -0,0 +1,34 @@ +{ + "name": "vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3010", + "build": "tsc -b && vite build", + "test": "npx playwright test", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-virtualized-auto-sizer": "workspace:*", + "react-router": "^7" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@playwright/test": "^1", + "@tailwindcss/vite": "^4.1.17", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "tailwindcss": "^4.1.17", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } +} diff --git a/integrations/vite/playwright.config.ts b/integrations/vite/playwright.config.ts new file mode 100644 index 0000000..4356231 --- /dev/null +++ b/integrations/vite/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + projects: [ + { + name: "chromium", + timeout: 5_000, + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1000, height: 600 } + + // Uncomment to visually debug + // headless: false, + // launchOptions: { + // slowMo: 500 + // } + } + } + ] +}); diff --git a/integrations/vite/src/components/Children.tsx b/integrations/vite/src/components/Children.tsx new file mode 100644 index 0000000..62e2d94 --- /dev/null +++ b/integrations/vite/src/components/Children.tsx @@ -0,0 +1,43 @@ +import { useLayoutEffect, useState } from "react"; + +export type SizeProps = { + height: number | undefined; + width: number | undefined; +}; + +export const Children = function Children({ + height, + onCommitLogsChange, + width +}: SizeProps & { + onCommitLogsChange: (logs: SizeProps[]) => void; +}) { + const [commitLogs, setCommitLogs] = useState([]); + + useLayoutEffect(() => { + setCommitLogs((prev) => [ + ...prev, + { + height: + height === undefined ? undefined : parseFloat(height.toFixed(1)), + width: width === undefined ? undefined : parseFloat(width.toFixed(1)) + } as SizeProps + ]); + }, [height, width]); + + useLayoutEffect(() => onCommitLogsChange(commitLogs)); + + // Account for StrictMode double rendering on mount + useLayoutEffect( + () => () => { + setCommitLogs([]); + }, + [] + ); + + return ( +
+ {width} x {height} pixels +
+ ); +}; diff --git a/integrations/vite/src/components/Container.tsx b/integrations/vite/src/components/Container.tsx new file mode 100644 index 0000000..9d0059b --- /dev/null +++ b/integrations/vite/src/components/Container.tsx @@ -0,0 +1,9 @@ +import { type PropsWithChildren } from "react"; + +export type ContainerProps = PropsWithChildren<{ + className?: string | undefined; +}>; + +export function Container({ children, className }: ContainerProps) { + return
{children}
; +} diff --git a/integrations/vite/src/components/DebugData.tsx b/integrations/vite/src/components/DebugData.tsx new file mode 100644 index 0000000..accf3f0 --- /dev/null +++ b/integrations/vite/src/components/DebugData.tsx @@ -0,0 +1,22 @@ +import { cn } from "../utils/cn"; + +export function DebugData({ data }: { data: object }) { + return ( +
+      {JSON.stringify(data, replacer, 2)}
+    
+ ); +} + +function replacer(_key: string, value: unknown) { + if (typeof value === "number") { + return Math.round(value); + } + + return value; +} diff --git a/integrations/vite/src/components/Resizer.tsx b/integrations/vite/src/components/Resizer.tsx new file mode 100644 index 0000000..c054f62 --- /dev/null +++ b/integrations/vite/src/components/Resizer.tsx @@ -0,0 +1,8 @@ +import { type PropsWithChildren } from "react"; + +export type ResizerProps = PropsWithChildren; + +export function Resizer({ children: childrenProp }: ResizerProps) { + // TODO + return childrenProp; +} diff --git a/integrations/vite/src/index.css b/integrations/vite/src/index.css new file mode 100644 index 0000000..522bae2 --- /dev/null +++ b/integrations/vite/src/index.css @@ -0,0 +1,23 @@ +@import "tailwindcss"; + +@layer base { + h1 { + @apply mb-4 text-4xl font-bold tracking-tight text-gray-900; + } + ul { + @apply list-disc pl-6; + } + ol { + @apply list-decimal pl-6; + } + p { + @apply mb-2 mt-2; + } + a { + @apply text-blue-600 hover:text-pink-400 visited:text-blue-900; + } +} + +#root { + height: 100vh; +} diff --git a/integrations/vite/src/main.tsx b/integrations/vite/src/main.tsx new file mode 100644 index 0000000..30740b3 --- /dev/null +++ b/integrations/vite/src/main.tsx @@ -0,0 +1,15 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter, Route, Routes } from "react-router"; +import "./index.css"; +import { Home } from "./routes/Home"; + +createRoot(document.getElementById("root")!).render( + + + + } /> + + + +); diff --git a/integrations/vite/src/routes/Home.tsx b/integrations/vite/src/routes/Home.tsx new file mode 100644 index 0000000..6b5b509 --- /dev/null +++ b/integrations/vite/src/routes/Home.tsx @@ -0,0 +1,85 @@ +import { + useCallback, + useLayoutEffect, + useMemo, + useState, + type FunctionComponent +} from "react"; +import { createPortal } from "react-dom"; +import { + AutoSizer, + type AutoSizerBox, + type Size +} from "react-virtualized-auto-sizer"; +import { Children, type SizeProps } from "../components/Children"; +import { useSearchParams } from "react-router"; + +export function Home() { + const [params] = useSearchParams(); + const box = (params.get("box") || undefined) as AutoSizerBox | undefined; + const style = params.get("style") || ""; + console.log("Home:", JSON.stringify({ box, style }, null, 2)); + + const [container] = useState(() => { + const div = document.createElement("div"); + div.setAttribute("data-testid", "container"); + div.style = `width: 100%; height: 100%; box-sizing: border-box; color: white; ${style}`; + return div; + }); + const [iframe, setIframe] = useState(null); + + useLayoutEffect(() => { + if (!iframe || !iframe.contentDocument) { + return; + } + + iframe.contentDocument.body.style = "margin: 0; padding: 0; height: 100vh;"; + iframe.contentDocument.body.appendChild(container); + }, [container, iframe]); + + const [commits, setCommits] = useState([]); + const [onResizeCalls, setOnResizeCalls] = useState([]); + + const Child = useMemo>( + () => + ({ height, width }) => ( + + ), + [] + ); + + const onResize = useCallback((size: Size) => { + setOnResizeCalls((prev) => [ + ...prev, + { + height: parseFloat(size.height.toFixed(1)), + width: parseFloat(size.width.toFixed(1)) + } + ]); + }, []); + + return ( +
+