Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .npmrc

This file was deleted.

2 changes: 1 addition & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "oxc.oxc-vscode"]
"recommendations": ["dbaeumer.vscode-eslint", "oxc.oxc-vscode", "typescriptteam.native-preview"]
}
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"editor.formatOnSaveMode": "file",
"js/ts.tsdk.promptToUseWorkspaceVersion": true,
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.experimental.useTsgo": true,
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
},
Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
```shell
npm install # setup (requires Node.js ≥ 22 for `node --run`)
node --run build # library → lib/
node --run typecheck # tsc --build
node --run typecheck # tsgo --build
node --run eslint # eslint --max-warnings 0
node --run eslint:fix # eslint --fix
node --run format # oxfmt
Expand Down Expand Up @@ -48,12 +48,12 @@ website/ # demo site (Vite + TanStack Router)
- **Light/dark mode** — handled via CSS `light-dark()` + `color-scheme`, not JS.
- **Accessibility first** — ARIA attributes (e.g. `aria-colindex`, `aria-rowindex`, `aria-selected`, roles) are required. Tests query by role.
- **Formatting** — oxfmt (not Prettier). **Linting** — ESLint (must pass with zero warnings).
- **Build** — Rolldown bundles library to `lib/`; `ecij` plugin prefixes classes with `rdg-{version}-` (dots→dashes) to avoid cross-version conflicts.
- **Build** — tsdown bundles library to `lib/`; `ecij` plugin prefixes classes with `rdg-{version}-` (dots→dashes) to avoid cross-version conflicts.

## Testing

- Browser tests use `vitest/browser` + Playwright. `test/setupBrowser.ts` configures `page.render()` via `vitest-browser-react` and registers custom locators via `locators.extend()` — prefer `page.getGrid()`, `page.getCell({ name })`, `page.getRow()`, `page.getHeaderCell()`, `page.getSelectedCell()`, etc. over raw `page.getByRole()`.
- Test helpers in `test/browser/utils.tsx`: `setup()`, `getRowWithCell()`, `getCellsAtRowIndex()`, `validateCellPosition()`, `scrollGrid()`, `tabIntoGrid()`, `testCount()`, `testRowCount()`.
- Browser tests use `vitest/browser` + Playwright. `test/setupBrowser.ts` configures `page.render()` via `vitest-browser-react` and registers custom locators via `locators.extend()` — prefer `page.getGrid()`, `page.getCell({ name })`, `page.getRow()`, `page.getHeaderCell()`, `page.getActiveCell()`, etc. over raw `page.getByRole()`.
- Test helpers in `test/browser/utils.tsx`: `setup()`, `getRowWithCell()`, `getCellsAtRowIndex()`, `validateCellPosition()`, `scrollGrid()`, `safeTab()`, `testCount()`, `testRowCount()`.
- `test/failOnConsole.ts` fails tests on unexpected console warnings/errors.
- **Never run visual regression tests** — screenshots are environment-dependent so visual regression tests must run in CI only.

Expand Down
144 changes: 44 additions & 100 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import eslintReact from '@eslint-react/eslint-plugin';
import markdown from '@eslint/markdown';
import vitest from '@vitest/eslint-plugin';
import jestDom from 'eslint-plugin-jest-dom';
import reactDom from 'eslint-plugin-react-dom';
import reactHooks from 'eslint-plugin-react-hooks';
import reactNamingConvention from 'eslint-plugin-react-naming-convention';
import reactRsc from 'eslint-plugin-react-rsc';
import reactWebApi from 'eslint-plugin-react-web-api';
import sonarjs from 'eslint-plugin-sonarjs';
import testingLibrary from 'eslint-plugin-testing-library';
import { defineConfig, globalIgnores } from 'eslint/config';
import tseslint from 'typescript-eslint';

Expand All @@ -29,10 +23,6 @@ export default defineConfig([
// @ts-expect-error
'react-hooks': reactHooks,
'@eslint-react': eslintReact,
'@eslint-react/rsc': reactRsc,
'@eslint-react/dom': reactDom,
'@eslint-react/web-api': reactWebApi,
'@eslint-react/naming-convention': reactNamingConvention,
sonarjs,
'@typescript-eslint': tseslint.plugin
},
Expand Down Expand Up @@ -288,7 +278,7 @@ export default defineConfig([
// https://www.eslint-react.xyz/docs/rules/overview
/*
// copy all the rules from the rules table for easy pasting
function getRules(id, prefix) {
function getRules(id) {
return (
Iterator.from(
document
Expand All @@ -298,38 +288,33 @@ function getRules(id, prefix) {
.querySelectorAll('tr a')
)
// map link to rule declaration
.map((a) => `'@eslint-react/${prefix}${a.textContent}': 1,`)
.map((a) => `'@eslint-react/${a.getAttribute('href')}': 1,`)
);
}
copy(
Iterator.from([
getRules('x-rules', ''),
getRules('rsc-rules', 'rsc/'),
getRules('dom-rules', 'dom/'),
getRules('web-api-rules', 'web-api/'),
getRules('naming-convention-rules', 'naming-convention/'),
getRules('x-rules'),
getRules('jsx-rules'),
getRules('rsc-rules'),
getRules('dom-rules'),
getRules('web-api-rules'),
getRules('naming-convention-rules'),
])
.flatMap((x) => x)
.toArray()
.join('\n')
);
*/
'@eslint-react/jsx-dollar': 1,
'@eslint-react/jsx-key-before-spread': 1,
'@eslint-react/jsx-no-comment-textnodes': 1,
'@eslint-react/jsx-shorthand-boolean': 1,
'@eslint-react/jsx-shorthand-fragment': 1,
'@eslint-react/component-hook-factories': 1,
'@eslint-react/error-boundaries': 1,
'@eslint-react/exhaustive-deps': 1,
'@eslint-react/immutability': 0,
'@eslint-react/immutability': 1,
'@eslint-react/no-access-state-in-setstate': 1,
'@eslint-react/no-array-index-key': 0,
'@eslint-react/no-children-count': 1,
'@eslint-react/no-children-for-each': 1,
'@eslint-react/no-children-map': 1,
'@eslint-react/no-children-only': 1,
'@eslint-react/no-children-prop': 1,
'@eslint-react/no-children-to-array': 1,
'@eslint-react/no-class-component': 1,
'@eslint-react/no-clone-element': 1,
Expand Down Expand Up @@ -367,7 +352,6 @@ copy(
'@eslint-react/no-unused-props': 1,
'@eslint-react/no-unused-state': 1,
'@eslint-react/no-use-context': 1,
'@eslint-react/no-useless-fragment': [1, { allowExpressions: false }],
'@eslint-react/prefer-destructuring-assignment': 1,
'@eslint-react/prefer-namespace-import': 1,
'@eslint-react/purity': 1,
Expand All @@ -378,32 +362,37 @@ copy(
'@eslint-react/unsupported-syntax': 1,
'@eslint-react/use-memo': 1,
'@eslint-react/use-state': 1,
'@eslint-react/rsc/function-definition': 1,
'@eslint-react/dom/no-dangerously-set-innerhtml': 1,
'@eslint-react/dom/no-dangerously-set-innerhtml-with-children': 1,
'@eslint-react/dom/no-find-dom-node': 1,
'@eslint-react/dom/no-flush-sync': 0,
'@eslint-react/dom/no-hydrate': 1,
'@eslint-react/dom/no-missing-button-type': 1,
'@eslint-react/dom/no-missing-iframe-sandbox': 1,
'@eslint-react/dom/no-namespace': 1,
'@eslint-react/dom/no-render': 1,
'@eslint-react/dom/no-render-return-value': 1,
'@eslint-react/dom/no-script-url': 1,
'@eslint-react/dom/no-string-style-prop': 1,
'@eslint-react/dom/no-unknown-property': 0,
'@eslint-react/dom/no-unsafe-iframe-sandbox': 1,
'@eslint-react/dom/no-unsafe-target-blank': 1,
'@eslint-react/dom/no-use-form-state': 1,
'@eslint-react/dom/no-void-elements-with-children': 1,
'@eslint-react/dom/prefer-namespace-import': 1,
'@eslint-react/web-api/no-leaked-event-listener': 1,
'@eslint-react/web-api/no-leaked-interval': 1,
'@eslint-react/web-api/no-leaked-resize-observer': 1,
'@eslint-react/web-api/no-leaked-timeout': 1,
'@eslint-react/naming-convention/context-name': 1,
'@eslint-react/naming-convention/id-name': 1,
'@eslint-react/naming-convention/ref-name': 1,
'@eslint-react/jsx-no-children-prop': 1,
'@eslint-react/jsx-no-children-prop-with-children': 1,
'@eslint-react/jsx-no-comment-textnodes': 1,
'@eslint-react/jsx-no-useless-fragment': [1, { allowExpressions: false }],
'@eslint-react/jsx-no-key-after-spread': 1,
'@eslint-react/jsx-no-namespace': 1,
'@eslint-react/rsc-function-definition': 1,
'@eslint-react/dom-no-dangerously-set-innerhtml': 1,
'@eslint-react/dom-no-dangerously-set-innerhtml-with-children': 1,
'@eslint-react/dom-no-find-dom-node': 1,
'@eslint-react/dom-no-flush-sync': 0,
'@eslint-react/dom-no-hydrate': 1,
'@eslint-react/dom-no-missing-button-type': 1,
'@eslint-react/dom-no-missing-iframe-sandbox': 1,
'@eslint-react/dom-no-render': 1,
'@eslint-react/dom-no-render-return-value': 1,
'@eslint-react/dom-no-script-url': 1,
'@eslint-react/dom-no-string-style-prop': 1,
'@eslint-react/dom-no-unknown-property': 1,
'@eslint-react/dom-no-unsafe-iframe-sandbox': 1,
'@eslint-react/dom-no-unsafe-target-blank': 1,
'@eslint-react/dom-no-use-form-state': 1,
'@eslint-react/dom-no-void-elements-with-children': 1,
'@eslint-react/dom-prefer-namespace-import': 1,
'@eslint-react/web-api-no-leaked-event-listener': 1,
'@eslint-react/web-api-no-leaked-interval': 1,
'@eslint-react/web-api-no-leaked-resize-observer': 1,
'@eslint-react/web-api-no-leaked-timeout': 1,
'@eslint-react/naming-convention-context-name': 1,
'@eslint-react/naming-convention-id-name': 1,
'@eslint-react/naming-convention-ref-name': 1,

// SonarJS rules
// https://github.com/SonarSource/SonarJS/blob/master/packages/jsts/src/rules/README.md#rules
Expand Down Expand Up @@ -893,7 +882,7 @@ copy(
1,
{ path: 'never', types: 'never', lib: 'never' }
],
'@typescript-eslint/unbound-method': 0,
'@typescript-eslint/unbound-method': 0, // replaced by vitest/unbound-method
'@typescript-eslint/unified-signatures': 0,
'@typescript-eslint/use-unknown-in-catch-callback-variable': 1
}
Expand All @@ -905,9 +894,7 @@ copy(
files: ['test/**/*'],

plugins: {
vitest,
'jest-dom': jestDom,
'testing-library': testingLibrary
vitest
},

rules: {
Expand Down Expand Up @@ -1022,55 +1009,12 @@ copy(
'vitest/require-test-timeout': 0,
'vitest/require-to-throw-message': 1,
'vitest/require-top-level-describe': 0,
'vitest/unbound-method': 0,
'vitest/valid-describe-callback': 1,
'vitest/valid-expect': [1, { alwaysAwait: true }],
'vitest/valid-expect-in-promise': 1,
'vitest/valid-title': 1,
'vitest/warn-todo': 1,

// https://github.com/testing-library/eslint-plugin-jest-dom#supported-rules
'jest-dom/prefer-checked': 1,
'jest-dom/prefer-empty': 1,
'jest-dom/prefer-enabled-disabled': 1,
'jest-dom/prefer-focus': 1,
'jest-dom/prefer-in-document': 1,
'jest-dom/prefer-required': 1,
'jest-dom/prefer-to-have-attribute': 1,
'jest-dom/prefer-to-have-class': 1,
'jest-dom/prefer-to-have-style': 1,
'jest-dom/prefer-to-have-text-content': 1,
'jest-dom/prefer-to-have-value': 1,

// eslint-plugin-testing-library Rules
// https://github.com/testing-library/eslint-plugin-testing-library#supported-rules
'testing-library/await-async-events': 0,
'testing-library/await-async-queries': 0,
'testing-library/await-async-utils': 0,
'testing-library/consistent-data-testid': 0,
'testing-library/no-await-sync-events': 0,
'testing-library/no-await-sync-queries': 0,
'testing-library/no-container': 1,
'testing-library/no-debugging-utils': 1,
'testing-library/no-dom-import': 1,
'testing-library/no-global-regexp-flag-in-query': 1,
'testing-library/no-manual-cleanup': 0,
'testing-library/no-node-access': 0,
'testing-library/no-promise-in-fire-event': 0,
'testing-library/no-render-in-lifecycle': 0,
'testing-library/no-test-id-queries': 0,
'testing-library/no-unnecessary-act': 1,
'testing-library/no-wait-for-multiple-assertions': 1,
'testing-library/no-wait-for-side-effects': 1,
'testing-library/no-wait-for-snapshot': 0,
'testing-library/prefer-explicit-assert': 1,
'testing-library/prefer-find-by': 1,
'testing-library/prefer-implicit-assert': 0,
'testing-library/prefer-presence-queries': 0,
'testing-library/prefer-query-by-disappearance': 1,
'testing-library/prefer-query-matchers': 0,
'testing-library/prefer-screen-queries': 0,
'testing-library/prefer-user-event': 1,
'testing-library/render-result-naming-convention': 0
'vitest/warn-todo': 1
}
},

Expand Down
29 changes: 14 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"start": "vite serve --clearScreen false",
"preview": "vite preview",
"build:website": "vite build",
"build": "rolldown -c",
"build": "tsdown",
"test": "vitest run --project browser --project node",
"test:watch": "vitest watch --project browser --project node",
"test:ci": "vitest run",
Expand All @@ -44,48 +44,47 @@
"format:check": "oxfmt --check",
"eslint": "eslint --max-warnings 0 --cache --cache-location .cache/eslint --cache-strategy content",
"eslint:fix": "node --run eslint -- --fix",
"typecheck": "tsc --build"
"typecheck": "tsgo --build"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^3.0.0",
"@eslint-react/eslint-plugin": "^4.2.1",
"@eslint/markdown": "^8.0.1",
"@faker-js/faker": "^10.3.0",
"@tanstack/react-router": "^1.166.7",
"@tanstack/router-plugin": "^1.166.7",
"@tsdown/css": "^0.21.7",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20260401.1",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/browser-playwright": "^4.1.0",
"@vitest/coverage-istanbul": "^4.1.0",
"@vitest/eslint-plugin": "^1.6.12",
"@vitest/browser-playwright": "^4.1.2",
"@vitest/coverage-istanbul": "^4.1.2",
"@vitest/eslint-plugin": "^1.6.13",
"clsx": "^2.1.1",
"ecij": "^0.4.1",
"eslint": "^10.0.3",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-sonarjs": "^4.0.2",
"eslint-plugin-testing-library": "^7.16.0",
"jspdf": "^4.2.0",
"jspdf-autotable": "^5.0.7",
"oxfmt": "0.43.0",
"playwright": "~1.59.0",
"postcss": "^8.5.2",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"rolldown": "1.0.0-rc.5",
"rolldown-plugin-dts": "^0.22.5",
"typescript": "~6.0.1-rc",
"typescript-eslint": "^8.57.0",
"vite": "^8.0.0",
"vitest": "^4.1.0",
"tsdown": "^0.21.7",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^8.0.3",
"vitest": "^4.1.2",
"vitest-browser-react": "^2.1.0"
},
"peerDependencies": {
"react": "^19.2",
"react-dom": "^19.2"
},
"overrides": {
"typescript": "$typescript"
"eslint": "$eslint"
}
}
2 changes: 1 addition & 1 deletion src/EditCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type {
* The event can be `stopPropagation()`ed halfway through, so they may not always bubble back up to the window,
* so an alternative check must be used. The check must happen after the event can reach the "inside" container,
* and not before it run to completion. `postTask`/`requestAnimationFrame` are the best way we know to achieve this.
* Usually we want click event handlers from parent components to access the latest commited values,
* Usually we want click event handlers from parent components to access the latest committed values,
* so `mousedown` is used instead of `click`.
*
* We must also rely on React's event capturing/bubbling to handle elements rendered in a portal.
Expand Down
2 changes: 1 addition & 1 deletion src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ declare module 'react' {
}
}

// somehow required to make types work
// required to make types work
export {};
2 changes: 1 addition & 1 deletion test/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ declare module 'vitest/browser' {
}
}

// somehow required to make types work
// required to make types work
export {};
1 change: 0 additions & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"noImplicitReturns": true,
"noUnusedLocals": true,
"outDir": "./.cache/ts",
"strictNullChecks": true, // TODO: remove once `typescript-eslint` supports TS 6
"target": "esnext",
"verbatimModuleSyntax": true
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"skipLibCheck": true,
"types": ["vitest/globals"]
},
"include": ["test/**/*"],
"include": ["src/css.d.ts", "test/**/*"],
"references": [{ "path": "tsconfig.src.json" }]
}
2 changes: 1 addition & 1 deletion tsconfig.vite.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"lib": ["ESNext", "DOM"],
"skipLibCheck": true
},
"include": ["package.json", "rolldown.config.ts", "vite.config.ts"]
"include": ["package.json", "tsdown.config.ts", "vite.config.ts"]
}
Loading
Loading