diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index c9b3f9a278..0c66abb4fe 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -105,6 +105,56 @@ jobs: name: ${{env.BUILD_LOGS}} path: ${{env.BUILD_LOGS}}.tar.br + React19: + # Forward-compatibility check: install React 19 + types into the cached + # node_modules (no save) and run typecheck + jest. Allowed to fail while + # the migration lands; flip to required once Repo 1 (backpack-web) is + # released and consumers can opt in. + runs-on: ubuntu-latest + needs: Build + continue-on-error: true + permissions: + statuses: write + pull-requests: write + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + + - name: Restore Cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: npm-cache + with: + path: | + node_modules/ + packages/node_modules/ + key: ${{ env.CACHE_NAME }}-${{ hashFiles('package-lock.json', 'packages/package-lock.json') }} + + - name: Override React to 19.2.5 + # The inner packages/node_modules tree has its own copy of react/react-dom + # populated from the cache. Overlay the root install into it so jest + # (which resolves react-dom from packages/) sees R19 too. + run: | + npm install --no-save react@19.2.5 react-dom@19.2.5 @types/react@19 @types/react-dom@19 @types/prop-types + rm -rf packages/node_modules/react packages/node_modules/react-dom + cp -r node_modules/react packages/node_modules/ + cp -r node_modules/react-dom packages/node_modules/ + + - name: Generate component code (icons, flare, spinners) + run: npm run build:gulp + + - name: Run typecheck (React 19) + run: npm run typecheck + + - name: Run jest (React 19) + run: TZ=Etc/UTC npm run jest + Danger: runs-on: ubuntu-latest needs: Build diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cb012ac95b..e2ab7d8887 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -179,6 +179,10 @@ jobs: private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Draft release notes - uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0 - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + uses: release-drafter/release-drafter@563bf132657a13ded0b01fcb723c5a58cdd824e2 # v7.2.1 + # release-drafter v7 no longer reads the GITHUB_TOKEN env var; the + # token must be passed via the `token` input or it falls back to the + # default `github.token`, which lacks the GitHub App permissions we + # rely on for drafting releases. + with: + token: ${{ steps.app-token.outputs.token }} diff --git a/REACT_19_MIGRATION.md b/REACT_19_MIGRATION.md new file mode 100644 index 0000000000..956e2aff1e --- /dev/null +++ b/REACT_19_MIGRATION.md @@ -0,0 +1,73 @@ +# React 19 Migration — `@skyscanner/backpack-web` + +(This repo is **Repo 1 of 5** in the Skyscanner shared-library React 19 pre-release codemod pass.) + +## What this thread should do + +1. Read the master plan's "Repo 1" section. It contains the full audit baseline, recipe checklist, and empty result fields to fill in. +2. Execute the recipe inside this directory. +3. As you progress, update the master plan's Repo 1 section with results (codemod diff summary, test outcomes, surprises). +4. When done, append a one-paragraph "Surprises / runbook insights" entry, and open follow-up PRs against `web-documentation` and `web-migration-scripts` if any reusable insights came out. + +## Audit baseline (2026-05-05) — corrected after re-audit on 2026-05-05 + +The original baseline overstated several categories. Re-audit confirmed: + +- Lerna-style monorepo, published from `packages/package.json` +- React peerDep: `17.0.2 - 18.3.1` → narrowed-and-shifted to `18.3.1 - 19.2.5` (We shouldn't have any consumers below 18) +- `static defaultProps` / `defaultProps =`: **84 files** (manual sweep, no codemod) +- `forwardRef`: 14 files (none combined with `defaultProps`) +- `prop-types` imports: **67 files** (mostly `.js`; manual conversion — see runbook insight below) +- `act` from `react-dom/test-utils`: **0** (audit said 7; re-grep finds none) +- `ReactDOM.render` / `hydrate`: **0** (audit said 1; the only hit is example text inside a `` JSX literal in `BpkCode.stories.tsx`, not a real call) +- String refs (`ref="..."`): **0** +- `findDOMNode`: **0** +- Legacy context API (`childContextTypes` / `getChildContext`): **0** +- `useFormState`: **0** +- Zero-arg `useRef()`: 0 +- TS 5.9.2; `@types/react` 18.3.1 +- Tests: Jest + `@testing-library/react` 16.3.0 +- CI: `.github/workflows/{release,pr,main}.yml` — `npm publish` after transpile + +### Runbook insight: `react/19/migration-recipe` is destructive on this codebase + +Dry-running `react/19/migration-recipe` (codemod 1.9.1, which silently invokes legacy codemod 0.18.13) on backpack: + +- **Strips Apache 2.0 license headers** from every modified file. Backpack requires these. +- Generates **wrong TypeScript interfaces** when files already have proper TS types (e.g. `BpkBasicMapMarker.tsx` keeps the existing `type Props = { children: ReactNode, position: LatLong }` orphaned and points the component at a new redundant `interface { ...; position: unknown }`). +- Only matched **6 of 67** prop-types files in this repo despite the recipe including `prop-types-typescript`. + +We are skipping the bundled recipe. The four mechanical sub-codemods (`replace-act-import`, `replace-reactdom-render`, `replace-string-ref`, `replace-use-form-state`) all dry-run as 0-changes here, so they are also skipped. Only `types-react-codemod` (TS-types only, much narrower scope) is being run. + +The `prop-types` and `defaultProps` migrations are being done with a custom jscodeshift transform tailored to backpack, preserving license headers and faithful types. + +### Runbook insight: peerDep range narrowed instead of widened + +Our intention is to **drop React 17 support** as part of this PR, to `18.3.1 - 19.2.5`. React 17 is unsupported upstream, no consumer is still on it, and dropping it shrinks the test/CI matrix surface. + +## Recipe summary — adapted for this repo + +1. [x] Pin `codemod` + `types-react-codemod` as devDeps with `--save-exact` (no `npx`/`pnpm dlx` — Skyscanner Security stance). +2. [x] ~~`react/19/migration-recipe`~~ — **skipped** (destructive on this codebase; see runbook insight above). +3. [x] ~~Individual mechanical codemods~~ — **skipped** (dry-runs as 0-changes for all four). +4. [x] `types-react-codemod preset-19` — applied with `refobject-defaults` and `useRef-required-initial` excluded (those two only typecheck against `@types/react@19`; deferred to the future `@types/react` bump PR). +5. [x] `types-react-codemod react-element-default-any-props` — confirmed no-op (already covered by preset-19). +6. [x] Custom jscodeshift transform applied (`scripts/react-19/transforms/strip-proptypes.js`, see scripts/react-19/README.md). Touched 27 files: removed prop-types from `.tsx`, migrated function-component `.defaultProps` to ES6 destructure defaults across `.js` and `.tsx`. License headers preserved. +7. [x] Manual cleanup of edge cases the transform left (1 eslint-disable + 2 dead-import removals). +8. [x] `forwardRef` ref-callback implicit-return scan — 0 issues, all 14 sites use block-body callbacks. +9. [x] Update `peerDependencies` in `packages/package.json` to `18.3.1 - 19.2.5` (drops React 17, adds 19; range syntax matching the existing peerDep style). +10. [x] Add CI matrix entry running tests against React 19.2.5 (`continue-on-error: true` initially). +11. [x] Uninstall codemod packages. +12. [ ] Coordinate version bump with the Backpack team. + +## Deferred to follow-up PRs + +The current PR establishes the scaffolding (peerDep range, CI matrix, codemod tooling, `.tsx` prop-types/defaultProps cleanup). The following items land in separate PRs: + +- **35 class components with `static defaultProps`** (27 `.tsx` + 8 `.js`) — React 19 makes these no-ops, so defaults silently stop applying. Each needs either conversion to a functional component (preferred) or destructure-with-defaults inside `render()` (quick fix). See the React19 CI matrix output for the failure surface; group by package to keep PRs reviewable. +- **13 `.js` story/HOC files with leftover `Component.defaultProps = ...`** — Phase B of the transform couldn't merge defaults because the function bodies are implicit-return arrows or otherwise ineligible. React 19 silently ignores these. +- **`.js` (Flow) prop-types removal** — intentionally skipped by the transform because the project's `react/prop-types` lint rule treats removed prop-types as missing prop validation on `.js` files. Full removal happens during the parallel TS migration; React 19 ignores `propTypes` silently in the meantime. +- **`@types/react@19` bump** — running `types-react-codemod preset-19`'s `refobject-defaults` and `useRef-required-initial` sub-transforms together with bumping `@types/react` to 19. These were skipped here because they emit code that only typechecks against `@types/react@19`. +- **Track and fix the React19 CI matrix failures** — typecheck has 8 known errors (the deferred sub-transforms above plus a missing `@types/prop-types`), and jest has 326 suite failures (mostly transitive deps still using removed React 18 internals like `ReactCurrentDispatcher`). Once green, flip the matrix from `continue-on-error: true` to required. +- **Move the custom transform to `web-migration-scripts/migrations/2026-05-react-19/transforms/`** when that migration directory is set up. + diff --git a/libs/backpack-storybook-utils/src/BpkDarkExampleWrapper.js b/libs/backpack-storybook-utils/src/BpkDarkExampleWrapper.js index 2cee510b31..da9a155da3 100644 --- a/libs/backpack-storybook-utils/src/BpkDarkExampleWrapper.js +++ b/libs/backpack-storybook-utils/src/BpkDarkExampleWrapper.js @@ -24,7 +24,7 @@ import STYLES from './BpkDarkExampleWrapper.module.scss'; const getClassName = cssModules(STYLES); const BpkDarkExampleWrapper = (props: { padded: boolean }) => { - const { padded, ...rest } = props; + const { padded = false, ...rest } = props; return ( /* $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'. */
{ ); }; -BpkDarkExampleWrapper.defaultProps = { - padded: false, -}; - export default BpkDarkExampleWrapper; diff --git a/package-lock.json b/package-lock.json index 81c190026f..bdb3f7ddad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,6 @@ "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "@types/react-transition-group": "^4.4.11", - "@types/react-window": "^1.8.8", "@types/webpack-env": "^1.18.4", "autoprefixer": "^10.4.18", "babel-loader": "^10.0.0", @@ -96,7 +95,7 @@ "style-loader": "^4.0.0", "ts-migrate": "^0.1.35", "tsx": "^4.19.2", - "typescript": "^5.9.2", + "typescript": "^6.0.3", "webpack": "^5.103.0", "wrapper-webpack-plugin": "^2.2.2" }, @@ -1010,11 +1009,13 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.22.5", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1521,12 +1522,14 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.22.5", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-flow": "^7.22.5" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2315,13 +2318,15 @@ } }, "node_modules/@babel/preset-flow": { - "version": "7.22.15", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.27.1.tgz", + "integrity": "sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-flow-strip-types": "^7.22.5" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-flow-strip-types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4094,6 +4099,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@figma/code-connect/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@figma/code-connect/node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -6873,6 +6892,20 @@ "node": ">=10" } }, + "node_modules/@nx/eslint/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@nx/jest": { "version": "22.7.1", "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-22.7.1.tgz", @@ -12298,15 +12331,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-window": { - "version": "1.8.8", - "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", - "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -12500,42 +12524,6 @@ } } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", - "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/scope-manager": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", @@ -12554,23 +12542,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", - "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, "node_modules/@typescript-eslint/type-utils": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", @@ -13099,6 +13070,45 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/utils": { "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", @@ -29011,12 +29021,6 @@ "dev": true, "license": "MIT" }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "license": "MIT" - }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -33651,20 +33655,13 @@ } }, "node_modules/react-window": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", - "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz", + "integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "memoize-one": ">=3.1.1 <6" - }, - "engines": { - "node": ">8.0.0" - }, "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/read-cache": { @@ -39454,9 +39451,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -41862,7 +41859,7 @@ "react-autosuggest": "^9.4.3", "react-table": "^7.8.0", "react-virtualized-auto-sizer": "1.0.20", - "react-window": "^1.8.7", + "react-window": "^2.0.0", "tabbable": "^6.4.0" }, "peerDependencies": { diff --git a/package.json b/package.json index 4c72690162..269ec46a0c 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,9 @@ "setupFilesAfterEnv": [ "/scripts/jest/setup.js" ], + "snapshotSerializers": [ + "/scripts/jest/normalizeUseIdSerializer.js" + ], "testEnvironment": "jsdom", "testRegex": "(?:packages|token-sync)/.*-test\\.[jt]sx?$", "transformIgnorePatterns": [ @@ -141,7 +144,6 @@ "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "@types/react-transition-group": "^4.4.11", - "@types/react-window": "^1.8.8", "@types/webpack-env": "^1.18.4", "autoprefixer": "^10.4.18", "babel-loader": "^10.0.0", @@ -186,7 +188,7 @@ "style-loader": "^4.0.0", "ts-migrate": "^0.1.35", "tsx": "^4.19.2", - "typescript": "^5.9.2", + "typescript": "^6.0.3", "webpack": "^5.103.0", "wrapper-webpack-plugin": "^2.2.2" }, diff --git a/packages/backpack-web/package.json b/packages/backpack-web/package.json index f8b94c3ff2..d873a5cd87 100644 --- a/packages/backpack-web/package.json +++ b/packages/backpack-web/package.json @@ -71,7 +71,7 @@ "react-autosuggest": "^9.4.3", "react-table": "^7.8.0", "react-virtualized-auto-sizer": "1.0.20", - "react-window": "^1.8.7" + "react-window": "^2.0.0" }, "peerDependencies": { "date-fns": "3.3.1 - 4", diff --git a/packages/backpack-web/react-version-test.js b/packages/backpack-web/react-version-test.js index 9d8d5f47d0..58d14d17e3 100644 --- a/packages/backpack-web/react-version-test.js +++ b/packages/backpack-web/react-version-test.js @@ -18,6 +18,6 @@ import { version } from 'react'; -it('packages/* should be ^18.0.0', () => { - expect(version).toMatch(/^18/); +it('packages/* should run on React 18 or 19', () => { + expect(version).toMatch(/^(18|19)\./); }); diff --git a/packages/backpack-web/src/bpk-component-accordion/src/BpkAccordionItem.tsx b/packages/backpack-web/src/bpk-component-accordion/src/BpkAccordionItem.tsx index 166d9ae657..74b959ffed 100644 --- a/packages/backpack-web/src/bpk-component-accordion/src/BpkAccordionItem.tsx +++ b/packages/backpack-web/src/bpk-component-accordion/src/BpkAccordionItem.tsx @@ -40,7 +40,7 @@ export type BpkAccordionItemProps = { className?: string; expanded?: boolean; initiallyExpanded?: boolean; - icon?: ReactElement; + icon?: ReactElement; onClick?: () => void; tagName?: 'span' | 'p' | 'text' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; textStyle?: (typeof TEXT_STYLES)[keyof typeof TEXT_STYLES]; diff --git a/packages/backpack-web/src/bpk-component-accordion/src/withAccordionItemState.tsx b/packages/backpack-web/src/bpk-component-accordion/src/withAccordionItemState.tsx index c2a7e5b494..b2717b371a 100644 --- a/packages/backpack-web/src/bpk-component-accordion/src/withAccordionItemState.tsx +++ b/packages/backpack-web/src/bpk-component-accordion/src/withAccordionItemState.tsx @@ -24,8 +24,8 @@ import { wrapDisplayName } from '../../bpk-react-utils'; import type { BpkAccordionItemProps } from './BpkAccordionItem'; type Props = { - initiallyExpanded: boolean; - expanded: boolean; + initiallyExpanded?: boolean; + expanded?: boolean; onClick?: () => void; }; @@ -42,17 +42,13 @@ const withAccordionItemState =

( 'withAccordionItemState', ); - static defaultProps = { - initiallyExpanded: false, - expanded: false, - onClick: null, - }; - constructor(props: P & Props) { super(props); + const { initiallyExpanded = false } = props; + this.state = { - expanded: props.initiallyExpanded, + expanded: initiallyExpanded, }; } diff --git a/packages/backpack-web/src/bpk-component-accordion/src/withSingleItemAccordionState.tsx b/packages/backpack-web/src/bpk-component-accordion/src/withSingleItemAccordionState.tsx index c89ffa3808..66189e05e4 100644 --- a/packages/backpack-web/src/bpk-component-accordion/src/withSingleItemAccordionState.tsx +++ b/packages/backpack-web/src/bpk-component-accordion/src/withSingleItemAccordionState.tsx @@ -24,10 +24,10 @@ import { wrapDisplayName } from '../../bpk-react-utils'; import type { BpkAccordionProps } from './BpkAccordion'; const getInitiallyExpanded = (children: ReactNode) => { - const accordionItems = Children.toArray(children) as ReactElement[]; + const accordionItems = Children.toArray(children) as Array>; const result = accordionItems.reduceRight( (prev, item) => (item.props.initiallyExpanded ? item : prev), - {} as ReactElement, + {} as ReactElement, ); return (result || {}).key || null; }; @@ -61,7 +61,7 @@ const withSingleItemAccordionState =

( this.setState({ expanded: key }); }; - renderAccordionItem = (accordionItem: ReactElement) => { + renderAccordionItem = (accordionItem: ReactElement) => { const expanded = this.state.expanded === accordionItem.key; const onClick = () => this.openAccordionItem(accordionItem?.key); @@ -74,7 +74,7 @@ const withSingleItemAccordionState =

( return ( {Children.toArray(children).map((el) => - this.renderAccordionItem(el as ReactElement), + this.renderAccordionItem(el as ReactElement), )} ); diff --git a/packages/backpack-web/src/bpk-component-ai-blurb/src/BpkAiBlurb-test.tsx b/packages/backpack-web/src/bpk-component-ai-blurb/src/BpkAiBlurb-test.tsx index f2b50100c7..6923e2e200 100644 --- a/packages/backpack-web/src/bpk-component-ai-blurb/src/BpkAiBlurb-test.tsx +++ b/packages/backpack-web/src/bpk-component-ai-blurb/src/BpkAiBlurb-test.tsx @@ -25,7 +25,7 @@ import { BpkProvider } from '../../bpk-component-layout'; import BpkAiBlurb from './BpkAiBlurb'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); describe('BpkAiBlurb.Root', () => { diff --git a/packages/backpack-web/src/bpk-component-ai-blurb/src/accessibility-test.tsx b/packages/backpack-web/src/bpk-component-ai-blurb/src/accessibility-test.tsx index 4f3c6373ef..9245677b02 100644 --- a/packages/backpack-web/src/bpk-component-ai-blurb/src/accessibility-test.tsx +++ b/packages/backpack-web/src/bpk-component-ai-blurb/src/accessibility-test.tsx @@ -26,7 +26,7 @@ import { BpkProvider } from '../../bpk-component-layout'; import BpkAiBlurb from './BpkAiBlurb'; -const renderWithProvider = (ui: ReactElement) => +const renderWithProvider = (ui: ReactElement) => render({ui}); describe('BpkAiBlurb accessibility tests', () => { diff --git a/packages/backpack-web/src/bpk-component-aria-live/src/BpkAriaLive.story-helpers.tsx b/packages/backpack-web/src/bpk-component-aria-live/src/BpkAriaLive.story-helpers.tsx index 3ce2cd1f02..0c422fc071 100644 --- a/packages/backpack-web/src/bpk-component-aria-live/src/BpkAriaLive.story-helpers.tsx +++ b/packages/backpack-web/src/bpk-component-aria-live/src/BpkAriaLive.story-helpers.tsx @@ -28,8 +28,8 @@ import STYLES from './BpkAriaLive.stories.module.scss'; const getClassName = cssModules(STYLES); type AriaLiveDemoProps = { - preamble?: ReactElement | null; - children: ReactElement; + preamble?: ReactElement | null; + children: ReactElement; className?: string | null; style?: {}; visible?: Boolean; diff --git a/packages/backpack-web/src/bpk-component-aria-live/src/BpkAriaLive.tsx b/packages/backpack-web/src/bpk-component-aria-live/src/BpkAriaLive.tsx index fd8d57f8c0..3d38217bdb 100644 --- a/packages/backpack-web/src/bpk-component-aria-live/src/BpkAriaLive.tsx +++ b/packages/backpack-web/src/bpk-component-aria-live/src/BpkAriaLive.tsx @@ -34,7 +34,7 @@ export type PolitenessSetting = (typeof POLITENESS_SETTINGS)[keyof typeof POLITENESS_SETTINGS]; export type Props = { - children: ReactElement | string; + children: ReactElement | string; politenessSetting?: PolitenessSetting; visible?: boolean; className?: string | null; diff --git a/packages/backpack-web/src/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.stories.tsx b/packages/backpack-web/src/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.stories.tsx index 1ea96138d1..840f445e2f 100644 --- a/packages/backpack-web/src/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.stories.tsx +++ b/packages/backpack-web/src/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.stories.tsx @@ -17,7 +17,7 @@ */ import { Component } from 'react'; -import type { ReactElement, InputHTMLAttributes, LegacyRef } from 'react'; +import type { ReactElement, InputHTMLAttributes, Ref } from 'react'; import { userEvent, within } from 'storybook/test'; @@ -178,10 +178,10 @@ type Props = { multiSection: boolean; renderInputComponent?: ( inputProps: InputHTMLAttributes & { - ref?: LegacyRef; + ref?: Ref; }, - ) => ReactElement; - renderSectionTitle: (section: Section) => ReactElement | null; + ) => ReactElement; + renderSectionTitle: (section: Section) => ReactElement | null; getSectionSuggestions: (section: Section) => Suggestion[]; }; @@ -321,7 +321,7 @@ export const HighlightFistSuggestion: Story = { // --- Multi-section example --- -const renderSectionTitle = (section: { title: string }): ReactElement => ( +const renderSectionTitle = (section: { title: string }): ReactElement => (

{section.title}
); @@ -374,7 +374,7 @@ export const SmallInput: Story = { const renderCustomInput = ( inputProps: InputHTMLAttributes & { - ref?: LegacyRef; + ref?: Ref; }, ) => (
= { }) => void; onSuggestionsFetchRequested: (value: string) => void; onSuggestionsClearRequested: () => void; - renderSuggestion: (suggestion: T) => ReactElement; + renderSuggestion: (suggestion: T) => ReactElement; id: string; enterKeyHint?: EnterKeyHintType; getA11yResultsMessage: (resultCount: number) => string; @@ -113,17 +113,17 @@ export type BpkAutoSuggestProps = { isDesktop?: boolean; onLoad?: (inputValue: string) => void; onClick?: () => void; - renderBesideInput?: () => ReactElement; + renderBesideInput?: () => ReactElement; showClear?: boolean; theme?: Partial; highlightFirstSuggestion?: boolean; shouldRenderSuggestions?: (value?: string) => boolean; multiSection?: boolean; getSectionSuggestions?: (section: T) => T[]; - renderSectionTitle?: (section: T) => ReactElement | null; + renderSectionTitle?: (section: T) => ReactElement | null; alwaysRenderSuggestions?: boolean; onInputValueChange?: (input: { method: string; newValue: string }) => void; - renderInputComponent?: (inputProps: BpkInputRenderProps) => ReactElement; + renderInputComponent?: (inputProps: BpkInputRenderProps) => ReactElement; onSuggestionHighlighted?: (data: { suggestion: T | null }) => void; focusInputOnSuggestionClick?: boolean; }; diff --git a/packages/backpack-web/src/bpk-component-banner-alert/src/AnimateAndFade.tsx b/packages/backpack-web/src/bpk-component-banner-alert/src/AnimateAndFade.tsx index 7b4079dbf2..57adb60a08 100644 --- a/packages/backpack-web/src/bpk-component-banner-alert/src/AnimateAndFade.tsx +++ b/packages/backpack-web/src/bpk-component-banner-alert/src/AnimateAndFade.tsx @@ -16,8 +16,8 @@ * limitations under the License. */ -import { Component } from 'react'; -import type { ReactNode } from 'react'; +import { Component, cloneElement, createRef, isValidElement } from 'react'; +import type { ReactElement, ReactNode, RefObject } from 'react'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; @@ -33,8 +33,8 @@ const getClassName = cssModules(STYLES); const ANIMATION_DURATION = parseInt(durationSm, 10); type Props = { - animateOnEnter: boolean; - animateOnLeave: boolean; + animateOnEnter?: boolean; + animateOnLeave?: boolean; children: ReactNode | string; show: boolean; className?: string | undefined; @@ -51,16 +51,12 @@ type State = { class AnimateAndFade extends Component { toggleImmediately: boolean; - static defaultProps = { - animateOnEnter: false, - animateOnLeave: false, - }; - constructor(props: Props) { super(props); - this.toggleImmediately = this.props.show && this.props.animateOnEnter; - const initiallyShown = this.toggleImmediately ? false : this.props.show; + const { animateOnEnter = false, show } = props; + this.toggleImmediately = show && animateOnEnter; + const initiallyShown = this.toggleImmediately ? false : show; this.state = { isExpanded: initiallyShown, @@ -110,6 +106,8 @@ class AnimateAndFade extends Component { } }; + nodeRef = createRef(); + toggle = () => { if (this.state.visible && this.state.isExpanded) { this.setState({ @@ -125,7 +123,7 @@ class AnimateAndFade extends Component { }; render() { - const { animateOnEnter, animateOnLeave, children, className } = this.props; + const { animateOnEnter = false, animateOnLeave = false, children, className } = this.props; const showPlaceholder = !this.state.visible && !this.state.hideAnimationInProgress; // While the expanding animation takes place, we render the child element @@ -147,6 +145,7 @@ class AnimateAndFade extends Component { > {this.state.visible && ( { exit: ANIMATION_DURATION * 2, }} > - {children} + {isValidElement(children) + ? cloneElement(children as ReactElement<{ ref?: RefObject }>, { ref: this.nodeRef }) + : children} )} diff --git a/packages/backpack-web/src/bpk-component-banner-alert/src/withBannerAlertState.tsx b/packages/backpack-web/src/bpk-component-banner-alert/src/withBannerAlertState.tsx index 45a8a0437f..3cc5c76ab4 100644 --- a/packages/backpack-web/src/bpk-component-banner-alert/src/withBannerAlertState.tsx +++ b/packages/backpack-web/src/bpk-component-banner-alert/src/withBannerAlertState.tsx @@ -48,22 +48,11 @@ const withBannerAlertState =

( hideIntervalId?: ReturnType | null; - static defaultProps = { - onDismiss: null, - onExpandToggle: null, - onHide: null, - expanded: false, - show: true, - hideAfter: null, - animateOnLeave: false, - children: null, - }; - constructor(props: P & WithBannerAlertStateProps) { super(props); this.state = { - expanded: props.expanded, + expanded: props.expanded ?? false, show: true, }; @@ -120,8 +109,8 @@ const withBannerAlertState =

( render() { const { - animateOnLeave, - children, + animateOnLeave = false, + children = null, expanded, hideAfter, onDismiss, diff --git a/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchart.js b/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchart.js index 9205f069c9..9e67df62d0 100644 --- a/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchart.js +++ b/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchart.js @@ -53,6 +53,16 @@ const getClassName = cssModules(STYLES); const spacing = remToPx('.375rem'); const lineHeight = remToPx(lineHeightSm); +const DEFAULT_X_AXIS_MARGIN = 2 * (lineHeight + spacing); +const DEFAULT_Y_AXIS_MARGIN = 4 * lineHeight + spacing; +const DEFAULT_Y_AXIS_DOMAIN = [null, null]; +const DEFAULT_GET_BAR_LABEL = ( + point: any, + xScaleDataKey: string, + yScaleDataKey: string, +) => `${point[xScaleDataKey]} - ${point[yScaleDataKey]}`; +const DEFAULT_GET_BAR_SELECTION = () => false; + const getMaxYValue = ( dataPoints: Array, outlierPercentage: ?number, @@ -108,30 +118,6 @@ class BpkBarchart extends Component { svgEl: ?Element; - static defaultProps = { - leadingScrollIndicatorClassName: null, - trailingScrollIndicatorClassName: null, - outlierPercentage: null, - showGridlines: false, - xAxisMargin: 2 * (lineHeight + spacing), - xAxisTickValue: identity, - xAxisTickOffset: 0, - xAxisTickEvery: 1, - yAxisMargin: 4 * lineHeight + spacing, - yAxisTickValue: identity, - yAxisNumTicks: null, - yAxisDomain: [null, null], - onBarClick: null, - onBarHover: null, - onBarFocus: null, - // Using type any here as xScaleDataKey or yScaleDataKey are strings and there is an issue that strings are not valid keys - getBarLabel: (point: any, xScaleDataKey: string, yScaleDataKey: string) => - `${point[xScaleDataKey]} - ${point[yScaleDataKey]}`, - getBarSelection: () => false, - BarComponent: BpkBarchartBar, - disableDataTable: false, - }; - constructor(props: Props) { super(props); @@ -167,31 +153,31 @@ class BpkBarchart extends Component { render() { const { - BarComponent, + BarComponent = BpkBarchartBar, data, - disableDataTable, - getBarLabel, - getBarSelection, + disableDataTable = false, + getBarLabel = DEFAULT_GET_BAR_LABEL, + getBarSelection = DEFAULT_GET_BAR_SELECTION, initialHeight, initialWidth, - leadingScrollIndicatorClassName, - onBarClick, - onBarFocus, - onBarHover, - outlierPercentage, - showGridlines, - trailingScrollIndicatorClassName, + leadingScrollIndicatorClassName = null, + onBarClick = null, + onBarFocus = null, + onBarHover = null, + outlierPercentage = null, + showGridlines = false, + trailingScrollIndicatorClassName = null, xAxisLabel, - xAxisMargin, - xAxisTickEvery, - xAxisTickOffset, - xAxisTickValue, + xAxisMargin = DEFAULT_X_AXIS_MARGIN, + xAxisTickEvery = 1, + xAxisTickOffset = 0, + xAxisTickValue = identity, xScaleDataKey, - yAxisDomain, + yAxisDomain = DEFAULT_Y_AXIS_DOMAIN, yAxisLabel, - yAxisMargin, - yAxisNumTicks, - yAxisTickValue, + yAxisMargin = DEFAULT_Y_AXIS_MARGIN, + yAxisNumTicks = null, + yAxisTickValue = identity, yScaleDataKey, ...rest } = this.props; diff --git a/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchartBar.js b/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchartBar.js index 8116dcd8aa..306fcdd02a 100644 --- a/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchartBar.js +++ b/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchartBar.js @@ -61,15 +61,15 @@ type Props = { const BpkBarchartBar = (props: Props) => { const { - className, + className = null, height, label, - onClick, - onFocus, - onHover, - outlier, - padding, - selected, + onClick = null, + onFocus = null, + onHover = null, + outlier = false, + padding = 0, + selected = false, width, x, y, @@ -142,14 +142,4 @@ BpkBarchartBar.propTypes = { selected: PropTypes.bool, }; -BpkBarchartBar.defaultProps = { - className: null, - onClick: null, - onHover: null, - onFocus: null, - outlier: false, - padding: 0, - selected: false, -}; - export default BpkBarchartBar; diff --git a/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchartBars.js b/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchartBars.js index f3bc70372f..6c64f59113 100644 --- a/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchartBars.js +++ b/packages/backpack-web/src/bpk-component-barchart/src/BpkBarchartBars.js @@ -76,15 +76,15 @@ const BpkBarchartBars = (props: Props) => { BarComponent, data, getBarLabel, - getBarSelection, + getBarSelection = () => false, height, - innerPadding, + innerPadding = 0.35, margin, maxYValue, - onBarClick, - onBarFocus, - onBarHover, - outerPadding, + onBarClick = null, + onBarFocus = null, + onBarHover = null, + outerPadding = 0.35, xScale, xScaleDataKey, yScale, @@ -154,13 +154,4 @@ BpkBarchartBars.propTypes = { onBarFocus: PropTypes.func, }; -BpkBarchartBars.defaultProps = { - outerPadding: 0.35, - innerPadding: 0.35, - onBarClick: null, - onBarHover: null, - onBarFocus: null, - getBarSelection: () => false, -}; - export default BpkBarchartBars; diff --git a/packages/backpack-web/src/bpk-component-barchart/src/BpkChartAxis.js b/packages/backpack-web/src/bpk-component-barchart/src/BpkChartAxis.js index 904dccd3fe..d39a836ad0 100644 --- a/packages/backpack-web/src/bpk-component-barchart/src/BpkChartAxis.js +++ b/packages/backpack-web/src/bpk-component-barchart/src/BpkChartAxis.js @@ -104,14 +104,14 @@ type Props = { const BpkChartAxis = (props: Props) => { const { height, - label, + label = null, margin, - numTicks, + numTicks = null, orientation, scale, - tickEvery, - tickOffset, - tickValue, + tickEvery = 1, + tickOffset = 0, + tickValue = identity, width, ...rest } = props; @@ -176,12 +176,4 @@ BpkChartAxis.propTypes = { tickEvery: PropTypes.number, }; -BpkChartAxis.defaultProps = { - tickOffset: 0, - tickEvery: 1, - tickValue: identity, - numTicks: null, - label: null, -}; - export default BpkChartAxis; diff --git a/packages/backpack-web/src/bpk-component-barchart/src/BpkChartDataTable.js b/packages/backpack-web/src/bpk-component-barchart/src/BpkChartDataTable.js index 4cc9a7bdab..0322397720 100644 --- a/packages/backpack-web/src/bpk-component-barchart/src/BpkChartDataTable.js +++ b/packages/backpack-web/src/bpk-component-barchart/src/BpkChartDataTable.js @@ -37,7 +37,7 @@ type Props = { }; const BpkChartDataTable = (props: Props) => { - const { data, xAxisLabel, xScaleDataKey, yAxisLabel, yScaleDataKey } = props; + const { data = null, xAxisLabel, xScaleDataKey, yAxisLabel, yScaleDataKey } = props; const rows = data.map((point, i) => { const key = `chart-data-table-row-${i}`; @@ -70,8 +70,4 @@ BpkChartDataTable.propTypes = { yAxisLabel: PropTypes.string.isRequired, }; -BpkChartDataTable.defaultProps = { - data: null, -}; - export default BpkChartDataTable; diff --git a/packages/backpack-web/src/bpk-component-barchart/src/BpkChartGridLines.js b/packages/backpack-web/src/bpk-component-barchart/src/BpkChartGridLines.js index 056a859b18..3092367536 100644 --- a/packages/backpack-web/src/bpk-component-barchart/src/BpkChartGridLines.js +++ b/packages/backpack-web/src/bpk-component-barchart/src/BpkChartGridLines.js @@ -49,11 +49,11 @@ const BpkChartGridLines = (props: Props) => { const { height, margin, - numTicks, + numTicks = null, orientation, scale, - tickEvery, - tickOffset, + tickEvery = 1, + tickOffset = 0, width, ...rest } = props; @@ -80,12 +80,12 @@ const BpkChartGridLines = (props: Props) => { const toLine = (tick, i) => ( // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'. - + />) ); return ( @@ -110,10 +110,4 @@ BpkChartGridLines.propTypes = { tickEvery: PropTypes.number, }; -BpkChartGridLines.defaultProps = { - numTicks: null, - tickOffset: 0, - tickEvery: 1, -}; - export default BpkChartGridLines; diff --git a/packages/backpack-web/src/bpk-component-bottom-sheet/src/BpkBottomSheet.tsx b/packages/backpack-web/src/bpk-component-bottom-sheet/src/BpkBottomSheet.tsx index ef5fff6ae4..44395f4da9 100644 --- a/packages/backpack-web/src/bpk-component-bottom-sheet/src/BpkBottomSheet.tsx +++ b/packages/backpack-web/src/bpk-component-bottom-sheet/src/BpkBottomSheet.tsx @@ -169,7 +169,7 @@ const BpkBottomSheet = ({ // For custom title (ReactNode), wrap it with an element that has the correct id // so BpkNavigationBar's aria-labelledby reference is valid const titleWithId = hasTitle && typeof title !== 'string' && isValidElement(title) - ? cloneElement(title as ReactElement, { id: showHiddenTitle ? hiddenTitleId : headingId }) + ? cloneElement(title as ReactElement, { id: showHiddenTitle ? hiddenTitleId : headingId }) : title; return ( diff --git a/packages/backpack-web/src/bpk-component-breakpoint/src/BpkBreakpoint.tsx b/packages/backpack-web/src/bpk-component-breakpoint/src/BpkBreakpoint.tsx index 01bc0a52fe..af359b38c6 100644 --- a/packages/backpack-web/src/bpk-component-breakpoint/src/BpkBreakpoint.tsx +++ b/packages/backpack-web/src/bpk-component-breakpoint/src/BpkBreakpoint.tsx @@ -77,15 +77,15 @@ const BpkBreakpoint = ({ useLegacyWarning(query, legacy, isClient); if (isClient) { if (typeof children === 'function') { - return children(matches) as ReactElement; + return children(matches) as ReactElement; } - return matches ? (children as ReactElement) : null; + return matches ? (children as ReactElement) : null; } if (typeof children === 'function') { - return children(!!matchSSR) as ReactElement; + return children(!!matchSSR) as ReactElement; } - return matchSSR ? (children as ReactElement) : null; + return matchSSR ? (children as ReactElement) : null; }; export { BREAKPOINTS }; export default BpkBreakpoint; diff --git a/packages/backpack-web/src/bpk-component-calendar/index.ts b/packages/backpack-web/src/bpk-component-calendar/index.ts index 5bf07c3e20..50ed215b9b 100644 --- a/packages/backpack-web/src/bpk-component-calendar/index.ts +++ b/packages/backpack-web/src/bpk-component-calendar/index.ts @@ -17,6 +17,9 @@ */ import BpkCalendarContainer, { + DEFAULT_MARK_TODAY, + DEFAULT_MAX_DATE, + DEFAULT_MIN_DATE, withCalendarState, } from './src/BpkCalendarContainer'; import BpkCalendarDate, { @@ -56,6 +59,9 @@ export { BpkCalendarDate, DateUtils, CALENDAR_SELECTION_TYPE, + DEFAULT_MARK_TODAY, + DEFAULT_MAX_DATE, + DEFAULT_MIN_DATE, DaysOfWeek, ReactComponent, WeekDay, diff --git a/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendar.stories.js b/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendar.stories.js index 135c643cd4..1013455184 100644 --- a/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendar.stories.js +++ b/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendar.stories.js @@ -253,7 +253,6 @@ const CustomComposedCalendarExample = () => ( const WeekExample = () => { const weekProps = { - ...BpkCalendarWeek.defaultProps, DateComponent: DummyDateComponent, dateModifiers: {}, dates: [ diff --git a/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendarContainer.tsx b/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendarContainer.tsx index ad9aec1b2b..84eb9b73cc 100644 --- a/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendarContainer.tsx +++ b/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendarContainer.tsx @@ -42,6 +42,15 @@ import { import type { SelectionConfiguration } from './custom-proptypes'; +export const DEFAULT_MARK_TODAY = true; +export const DEFAULT_MAX_DATE = addMonths(new Date(), 12); +export const DEFAULT_MIN_DATE = new Date(); + +const DEFAULT_SELECTION_CONFIGURATION: SelectionConfiguration = { + type: CALENDAR_SELECTION_TYPE.single, + date: null, +}; + export type Props = { /** * If set to true (default), it sets a fixed width on the calendar container. This is necessary to support transitions and to create the right size for the Datepicker component. @@ -177,38 +186,26 @@ const getRawSelectedDate = (selectionConfig: SelectionConfiguration) => { const withCalendarState =

(Calendar: ComponentType

) => { class BpkCalendarContainer extends Component, State> { - static defaultProps = { - fixedWidth: true, - maxDate: addMonths(new Date(), 12), - minDate: new Date(), - onDateSelect: null, - onMonthChange: null, - selectionConfiguration: { - type: CALENDAR_SELECTION_TYPE.single, - date: null, - }, - initiallyFocusedDate: null, - markToday: true, - markOutsideDays: true, - }; - constructor(props: CalendarProps

) { super(props); - const minDate = startOfDay(this.props.minDate!); - const maxDate = startOfDay(this.props.maxDate!); + const { + initiallyFocusedDate = null, + maxDate: maxDateProp = DEFAULT_MAX_DATE, + minDate: minDateProp = DEFAULT_MIN_DATE, + selectionConfiguration = DEFAULT_SELECTION_CONFIGURATION, + } = this.props; - const rawSelectedDate = getRawSelectedDate( - this.props.selectionConfiguration!, - ); + const minDate = startOfDay(minDateProp); + const maxDate = startOfDay(maxDateProp); - const { initiallyFocusedDate } = this.props; + const rawSelectedDate = getRawSelectedDate(selectionConfiguration); this.state = { preventKeyboardFocus: true, focusedDate: determineFocusedDate( rawSelectedDate[0], - initiallyFocusedDate!, + initiallyFocusedDate, minDate, maxDate, ), @@ -216,12 +213,16 @@ const withCalendarState =

(Calendar: ComponentType

) => { } componentDidUpdate(prevProps: CalendarProps

) { - const rawNextSelectedDate = getRawSelectedDate( - this.props.selectionConfiguration!, - ); + const { + maxDate: maxDateProp = DEFAULT_MAX_DATE, + minDate: minDateProp = DEFAULT_MIN_DATE, + selectionConfiguration = DEFAULT_SELECTION_CONFIGURATION, + } = this.props; + + const rawNextSelectedDate = getRawSelectedDate(selectionConfiguration); - const minDate = startOfDay(this.props.minDate!); - const maxDate = startOfDay(this.props.maxDate!); + const minDate = startOfDay(minDateProp); + const maxDate = startOfDay(maxDateProp); if ( focusedDateHasChanged( @@ -243,11 +244,15 @@ const withCalendarState =

(Calendar: ComponentType

) => { event: UIEvent, { date, source }: { date: Date; source: string }, ) => { - const { onMonthChange } = this.props; + const { + maxDate = DEFAULT_MAX_DATE, + minDate = DEFAULT_MIN_DATE, + onMonthChange = null, + } = this.props; const focusedDate = dateToBoundaries( date, - startOfDay(this.props.minDate!), - startOfDay(this.props.maxDate!), + startOfDay(minDate), + startOfDay(maxDate), ); const didMonthChange = !isSameMonth(this.state.focusedDate, focusedDate); @@ -265,18 +270,22 @@ const withCalendarState =

(Calendar: ComponentType

) => { }; handleDateSelect = (date: Date) => { - const { onDateSelect, selectionConfiguration } = this.props; + const { + maxDate = DEFAULT_MAX_DATE, + minDate = DEFAULT_MIN_DATE, + onDateSelect = null, + selectionConfiguration = DEFAULT_SELECTION_CONFIGURATION, + } = this.props; const keyboardFocusState = { preventKeyboardFocus: false }; if (onDateSelect) { const newDate = dateToBoundaries( date, - startOfDay(this.props.minDate!), - startOfDay(this.props.maxDate!), + startOfDay(minDate), + startOfDay(maxDate), ); if ( - selectionConfiguration && selectionConfiguration.type === CALENDAR_SELECTION_TYPE.range && selectionConfiguration.startDate && !selectionConfiguration.endDate && @@ -373,16 +382,20 @@ const withCalendarState =

(Calendar: ComponentType

) => { render() { const { - maxDate, - minDate, + fixedWidth = true, + initiallyFocusedDate = null, + markOutsideDays = true, + markToday = DEFAULT_MARK_TODAY, + maxDate = DEFAULT_MAX_DATE, + minDate = DEFAULT_MIN_DATE, onDateSelect, onMonthChange, - selectionConfiguration, + selectionConfiguration = DEFAULT_SELECTION_CONFIGURATION, ...calendarProps } = this.props; - const sanitisedMinDate = startOfDay(minDate!); - const sanitisedMaxDate = startOfDay(maxDate!); + const sanitisedMinDate = startOfDay(minDate); + const sanitisedMaxDate = startOfDay(maxDate); const sanitisedFocusedDate = dateToBoundaries( this.state.focusedDate, @@ -399,6 +412,10 @@ const withCalendarState =

(Calendar: ComponentType

) => { onDateKeyDown={this.handleDateKeyDown} preventKeyboardFocus={this.state.preventKeyboardFocus} focusedDate={sanitisedFocusedDate} + fixedWidth={fixedWidth} + initiallyFocusedDate={initiallyFocusedDate} + markOutsideDays={markOutsideDays} + markToday={markToday} {...(calendarProps as P)} minDate={sanitisedMinDate} maxDate={sanitisedMaxDate} diff --git a/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendarDate.tsx b/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendarDate.tsx index adb6a3d79e..8c0e62324b 100644 --- a/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendarDate.tsx +++ b/packages/backpack-web/src/bpk-component-calendar/src/BpkCalendarDate.tsx @@ -78,25 +78,14 @@ const navigatedByMonthNudger = () => document?.activeElement?.id && document.activeElement.id.indexOf('month_nudger') !== -1; -class BpkCalendarDate extends PureComponent { - static defaultProps: DefaultProps = { - className: null, - isBlocked: false, - isFocused: false, - isKeyboardFocusable: true, - isOutside: false, - isSelected: false, - isToday: false, - modifiers: {}, - onClick: null, - onDateKeyDown: () => {}, - preventKeyboardFocus: true, - selectionType: SELECTION_TYPES.none, - style: {}, - }; +const EMPTY_MODIFIERS: DateModifiers = {}; +const EMPTY_STYLE = {}; +const noopKeyDown = () => {}; +class BpkCalendarDate extends PureComponent { componentDidMount() { - if (!this.props.preventKeyboardFocus && this.props.isFocused) { + const { isFocused = false, preventKeyboardFocus = true } = this.props; + if (!preventKeyboardFocus && isFocused) { // If we got here by clicking the nudger, don't focus this date if (!navigatedByMonthNudger()) { // Giving focus after instantiation @@ -106,30 +95,29 @@ class BpkCalendarDate extends PureComponent { } componentDidUpdate(prevProps: Props) { + const { + isFocused = false, + isKeyboardFocusable = true, + preventKeyboardFocus = true, + } = this.props; + const { isFocused: prevIsFocused = false, isKeyboardFocusable: prevIsKeyboardFocusable = true } = prevProps; + if ( - !this.props.isKeyboardFocusable || - this.props.preventKeyboardFocus || + !isKeyboardFocusable || + preventKeyboardFocus || navigatedByMonthNudger() ) { return; } // Giving focus after keyboard navigation - if ( - !prevProps.isFocused && - this.props.isFocused && - this.props.isKeyboardFocusable - ) { + if (!prevIsFocused && isFocused && isKeyboardFocusable) { this.button?.focus(); return; } // Giving focus after changing months with transition - if ( - this.props.isFocused && - !prevProps.isKeyboardFocusable && - this.props.isKeyboardFocusable - ) { + if (isFocused && !prevIsKeyboardFocusable && isKeyboardFocusable) { this.button?.focus(); } } @@ -138,26 +126,25 @@ class BpkCalendarDate extends PureComponent { render() { const { - className, + className = null, date, - isBlocked, - isFocused, - isKeyboardFocusable, - isOutside, - isSelected, - isToday, - modifiers, - onClick, - onDateKeyDown, - selectionType, - style, + isBlocked = false, + isFocused = false, + isKeyboardFocusable = true, + isOutside = false, + isSelected = false, + modifiers = EMPTY_MODIFIERS, + onClick = null, + onDateKeyDown = noopKeyDown, + selectionType = SELECTION_TYPES.none, + style = EMPTY_STYLE, ...buttonProps } = this.props; const classNames = [getClassName('bpk-calendar-date')]; - Object.keys(modifiers!).forEach((modifier) => { - if (modifiers![modifier](this.props)) { + Object.keys(modifiers).forEach((modifier) => { + if (modifiers[modifier](this.props)) { classNames.push( getClassName(`bpk-calendar-date--modifier-${modifier}`), ); @@ -187,6 +174,7 @@ class BpkCalendarDate extends PureComponent { delete buttonProps.preventKeyboardFocus; delete buttonProps.isoLabel; + delete buttonProps.isToday; return ( +

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
- February 2010 - - + +
+
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
+ aria-hidden="false" + class="bpk-calendar-week__date bpk-calendar-week__date--none" + role="gridcell" + > + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
- - -
-
- -

+ +

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
- March 2010 - - + +
+
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- -`; - -exports[`BpkScrollableCalendar should render ranges correctly 1`] = ` - -
- -
-