From 4a37daeade3c4d43da9e045441ff974d8acf1072 Mon Sep 17 00:00:00 2001 From: Stefan Cameron Date: Sat, 7 Dec 2024 14:35:11 -0600 Subject: [PATCH] MVP support for React 19 (#1350) focus-trap-react was already "ready" for React 19. All that was necessary was to drop `prop-types`, which has been removed as a dependency. It's also time to move forward and stop supporting ancient versions of React. Therefore, the minimum supported version of React will now be 18.0.0, and I intend for that remain one version behind the latest version going forward, provided the gap between the two is reasonably surmountable. --- .changeset/hot-planes-roll.md | 5 +++ .eslintrc.js | 4 ++ .gitignore | 1 + README.md | 15 +++++-- index.d.ts | 4 +- package-lock.json | 74 +++++++++++++++------------------- package.json | 12 +++--- src/focus-trap-react.js | 76 ++++------------------------------- 8 files changed, 70 insertions(+), 121 deletions(-) create mode 100644 .changeset/hot-planes-roll.md diff --git a/.changeset/hot-planes-roll.md b/.changeset/hot-planes-roll.md new file mode 100644 index 00000000..ce393ad5 --- /dev/null +++ b/.changeset/hot-planes-roll.md @@ -0,0 +1,5 @@ +--- +'focus-trap-react': major +--- + +Dropping `propTypes` and `defaultProps` no longer supported by React 19 and long deprecated in React 18 (going forward, use TypeScript for prop typings, and if necessary, a runtime library to validate props); Increasing minimum supported React version up to >=18; Bumping `focus-trap` dependency to v7.6.2 diff --git a/.eslintrc.js b/.eslintrc.js index fd6dd01b..513e48ed 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -105,6 +105,10 @@ module.exports = { 'jest/no-focused-tests': 'error', 'jest/no-identical-title': 'error', 'jest/valid-title': 'error', + + //// from react plugin + + 'react/prop-types': 'off', // React 19 no longer supports propTypes }, settings: { // eslint-plugin-react settings: a version needs to be specified, diff --git a/.gitignore b/.gitignore index 83a8217e..74437977 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ dist/ cypress/videos cypress/screenshots +cypress/downloads diff --git a/README.md b/README.md index 2c2b784f..9ab51bed 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,23 @@ npm install focus-trap-react ### React dependency -React `>= 16.3.0` +React `>= 18.0.0` -## Browser Support +> Note that while React 18.x still supported `propTypes` and `defaultProps`, they had long-since been deprecated, and are completely dropped in React 19. + +Therefore, this library no longer assigns these properties to the `` element for runtime validation and initialization. The same techniques you would now use in React 19 are backward-compatible with React 18: + +- Use TypeScript for static prop type validation +- Use a runtime validation library such as [RTV.js](https://rtvjs.stefcameron.com/), [JSON Schema](https://json-schema.org/), or [yup](https://github.com/jquense/yup) for runtime prop validation to replace `prop-types`) -As old and as broad as _reasonably_ possible, excluding browsers that are out of support or have nearly no user base. +> This library aims to support one major version of React _behind_ the current major version, since React major releases are typically years apart -- to the extent that the feature drift is not too great and remains reasonably surmountable. + +## Browser Support Focused on desktop browsers, particularly Chrome, Edge, FireFox, Safari, and Opera. +Gated by what React [supports](https://legacy.reactjs.org/docs/javascript-environment-requirements.html) in the version [currently](#react-dependency) supported. + Focus-trap-react is not officially tested on any mobile browsers or devices. > ⚠️ Microsoft [no longer supports](https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know/) any version of IE, so IE is no longer supported by this library. diff --git a/index.d.ts b/index.d.ts index 92740328..6afe9355 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,8 +1,6 @@ import { Options as FocusTrapOptions } from 'focus-trap'; import * as React from 'react'; -export = FocusTrap; - declare namespace FocusTrap { export interface Props extends React.AllHTMLAttributes { children?: React.ReactNode; @@ -13,4 +11,4 @@ declare namespace FocusTrap { } } -declare class FocusTrap extends React.Component { } +export declare class FocusTrap extends React.Component { } diff --git a/package-lock.json b/package-lock.json index d564c04d..98ce70dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,9 +23,11 @@ "@testing-library/cypress": "^10.0.2", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.0.1", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", "@types/jquery": "^3.5.32", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", "all-contributors-cli": "^6.26.1", "babel-jest": "^29.7.0", "babelify": "^10.0.0", @@ -43,7 +45,6 @@ "jest-watch-typeahead": "^2.2.2", "onchange": "^7.1.0", "prettier": "^3.4.1", - "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "regenerator-runtime": "^0.14.1", @@ -51,9 +52,10 @@ "typescript": "^5.7.2" }, "peerDependencies": { - "prop-types": "^15.8.1", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -3673,10 +3675,11 @@ } }, "node_modules/@testing-library/react": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", - "integrity": "sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.1.0.tgz", + "integrity": "sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -3685,10 +3688,10 @@ }, "peerDependencies": { "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3837,42 +3840,29 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@types/react": { - "version": "18.0.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.31.tgz", - "integrity": "sha512-EEG67of7DsvRDU6BLLI0p+k1GojDLz9+lZsnCpCRTa/lOokvyPBvp8S5x+A24hME3yyQuIipcP70KJ6H7Qupww==", + "version": "18.3.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.14.tgz", + "integrity": "sha512-NzahNKvjNhVjuPBQ+2G7WlxstQ+47kXZNHlUvFakDViuIEfGY926GqhMueQFZ7woG+sPiQKlF36XfrIUVSUfFg==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.0.11", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", - "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", + "version": "18.3.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.2.tgz", + "integrity": "sha512-Fqp+rcvem9wEnGr3RY8dYNvSQ8PoLqjZ9HLgaPUOjJJD120uDyOxOjc/39M4Kddp9JQCxpGQbnhVQF0C0ncYVg==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { - "@types/react": "*" + "@types/react": "^18" } }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -6017,9 +6007,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/cypress": { "version": "13.16.0", @@ -6488,10 +6476,11 @@ } }, "node_modules/default-gateway/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -14037,6 +14026,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -14049,6 +14039,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -14578,6 +14569,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } diff --git a/package.json b/package.json index 67816f46..48e6b217 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,11 @@ "@testing-library/cypress": "^10.0.2", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.0.1", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", "@types/jquery": "^3.5.32", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", "all-contributors-cli": "^6.26.1", "babel-jest": "^29.7.0", "babelify": "^10.0.0", @@ -87,7 +89,6 @@ "jest-watch-typeahead": "^2.2.2", "onchange": "^7.1.0", "prettier": "^3.4.1", - "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "regenerator-runtime": "^0.14.1", @@ -99,8 +100,9 @@ "tabbable": "^6.2.0" }, "peerDependencies": { - "prop-types": "^15.8.1", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } } diff --git a/src/focus-trap-react.js b/src/focus-trap-react.js index c11345ce..971c918b 100644 --- a/src/focus-trap-react.js +++ b/src/focus-trap-react.js @@ -1,8 +1,10 @@ const React = require('react'); -const PropTypes = require('prop-types'); const { createFocusTrap } = require('focus-trap'); const { isFocusable } = require('tabbable'); +/** + * @type {import('../index.d.ts').FocusTrap} + */ class FocusTrap extends React.Component { constructor(props) { super(props); @@ -412,74 +414,10 @@ class FocusTrap extends React.Component { } } -// support server-side rendering where `Element` will not be defined -const ElementType = typeof Element === 'undefined' ? Function : Element; - -FocusTrap.propTypes = { - active: PropTypes.bool, - paused: PropTypes.bool, - focusTrapOptions: PropTypes.shape({ - document: PropTypes.object, - onActivate: PropTypes.func, - onPostActivate: PropTypes.func, - checkCanFocusTrap: PropTypes.func, - onPause: PropTypes.func, - onPostPause: PropTypes.func, - onUnpause: PropTypes.func, - onPostUnpause: PropTypes.func, - onDeactivate: PropTypes.func, - onPostDeactivate: PropTypes.func, - checkCanReturnFocus: PropTypes.func, - initialFocus: PropTypes.oneOfType([ - PropTypes.instanceOf(ElementType), - PropTypes.string, - PropTypes.bool, - PropTypes.func, - ]), - fallbackFocus: PropTypes.oneOfType([ - PropTypes.instanceOf(ElementType), - PropTypes.string, - // NOTE: does not support `false` as value (or return value from function) - PropTypes.func, - ]), - escapeDeactivates: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - clickOutsideDeactivates: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.func, - ]), - returnFocusOnDeactivate: PropTypes.bool, - setReturnFocus: PropTypes.oneOfType([ - PropTypes.instanceOf(ElementType), - PropTypes.string, - PropTypes.bool, - PropTypes.func, - ]), - allowOutsideClick: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - preventScroll: PropTypes.bool, - tabbableOptions: PropTypes.shape({ - displayCheck: PropTypes.oneOf([ - 'full', - 'legacy-full', - 'non-zero-area', - 'none', - ]), - getShadowRoot: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - }), - trapStack: PropTypes.array, - isKeyForward: PropTypes.func, - isKeyBackward: PropTypes.func, - }), - containerElements: PropTypes.arrayOf(PropTypes.instanceOf(ElementType)), // DOM element ONLY - children: PropTypes.oneOfType([ - PropTypes.element, // React element - PropTypes.instanceOf(ElementType), // DOM element - ]), - - // NOTE: _createFocusTrap is internal, for testing purposes only, so we don't - // specify it here. It's expected to be set to the function returned from - // require('focus-trap'), or one with a compatible interface. -}; - +// NOTE: While React 19 REMOVED support for `propTypes`, support for `defaultProps` +// __for class components ONLY__ remains: "Class components will continue to support +// defaultProps since there is no ES6 alternative." +// @see https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-proptypes-and-defaultprops FocusTrap.defaultProps = { active: true, paused: false,