diff --git a/.gitattributes b/.gitattributes index 0dd26ee2c3..31d27a0985 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ amd/* linguist-generated=true -es/* linguist-generated=true +es/**/* linguist-generated=true dist/* linguist-generated=true -lib/* linguist-generated=true +lib/**/* linguist-generated=true diff --git a/karma.conf.js b/karma.conf.js index f26089096a..65b9cbf1ad 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -68,7 +68,7 @@ module.exports = config => { } }, - browsers: env.BROWSER ? env.BROWSER.split(',') : ['Chrome'], + browsers: env.BROWSER ? env.BROWSER.split(',') : ['ChromeHeadless'], singleRun: env.CONTINUOUS_INTEGRATION === 'true' }); diff --git a/package.json b/package.json index c2dc55bad4..0ff24ea394 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "@babel/preset-env": "^7.0.0", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.0.0", - "@cfaester/enzyme-adapter-react-18": "^0.8.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -82,7 +81,6 @@ "colors": "^1.2.1", "create-react-class": "^15.6.3", "cross-env": "^5.2.0", - "enzyme": "^3.1.0", "eslint": "^4.19.1", "eslint-config-airbnb": "^16.1.0", "eslint-config-prettier": "^2.9.0", @@ -110,9 +108,9 @@ "mocha": "^5.2.0", "patch-package": "^8.0.0", "prettier": "^1.12.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-test-renderer": "^18.3.1", + "react": "19.2.7", + "react-dom": "19.2.7", + "react-test-renderer": "19.2.7", "release-script": "^1.0.2", "sinon": "^2.3.2", "sinon-chai": "^2.8.0", @@ -126,9 +124,9 @@ "keycode": "^2.2.0", "prop-types": "^15.6.1", "prop-types-extra": "^1.0.1", - "react-overlays": "^0.9.3", + "react-overlays": "^5.2.0", "react-prop-types": "^0.4.0", - "react-transition-group": "^2.0.0", + "react-transition-group": "^4.4.0", "uncontrollable": "^5.0.0", "warning": "^3.0.0" }, diff --git a/patches/react-overlays+0.9.3.patch b/patches/react-overlays+0.9.3.patch deleted file mode 100644 index 5539968862..0000000000 --- a/patches/react-overlays+0.9.3.patch +++ /dev/null @@ -1,35 +0,0 @@ -diff --git a/node_modules/react-overlays/lib/Modal.js b/node_modules/react-overlays/lib/Modal.js -index 9fbba3b..c876341 100644 ---- a/node_modules/react-overlays/lib/Modal.js -+++ b/node_modules/react-overlays/lib/Modal.js -@@ -576,7 +576,7 @@ var _initialiseProps = function _initialiseProps() { - }; - - this.handleDocumentKeyDown = function (e) { -- if (_this2.props.keyboard && e.keyCode === 27 && _this2.isTopModal()) { -+ if (_this2.props.keyboard && (e.keyCode === 27 || e.key === 'Escape') && _this2.isTopModal()) { - if (_this2.props.onEscapeKeyDown) { - _this2.props.onEscapeKeyDown(e); - } -@@ -586,7 +586,7 @@ var _initialiseProps = function _initialiseProps() { - }; - - this.handleDocumentKeyUp = function (e) { -- if (_this2.props.keyboard && e.keyCode === 27 && _this2.isTopModal()) { -+ if (_this2.props.keyboard && (e.keyCode === 27 || e.key === 'Escape') && _this2.isTopModal()) { - if (_this2.props.onEscapeKeyUp) { - _this2.props.onEscapeKeyUp(e); - } -diff --git a/node_modules/react-overlays/lib/RootCloseWrapper.js b/node_modules/react-overlays/lib/RootCloseWrapper.js -index 683e1cf..c561092 100644 ---- a/node_modules/react-overlays/lib/RootCloseWrapper.js -+++ b/node_modules/react-overlays/lib/RootCloseWrapper.js -@@ -115,7 +115,7 @@ var RootCloseWrapper = function (_React$Component) { - return; - } - -- if (e.keyCode === escapeKeyCode && _this.props.onRootClose) { -+ if ((e.keyCode === escapeKeyCode || e.key === 'Escape') && _this.props.onRootClose) { - _this.props.onRootClose(e); - } - }; diff --git a/patches/react-overlays+5.2.1.patch b/patches/react-overlays+5.2.1.patch new file mode 100644 index 0000000000..3789e904b2 --- /dev/null +++ b/patches/react-overlays+5.2.1.patch @@ -0,0 +1,52 @@ +diff --git a/node_modules/react-overlays/cjs/Modal.js b/node_modules/react-overlays/cjs/Modal.js +index 2247ad0..71c9f4a 100644 +--- a/node_modules/react-overlays/cjs/Modal.js ++++ b/node_modules/react-overlays/cjs/Modal.js +@@ -215,7 +215,7 @@ var Modal = /*#__PURE__*/(0, _react.forwardRef)(function (_ref, ref) { + } + }); + var handleDocumentKeyDown = (0, _useEventCallback["default"])(function (e) { +- if (keyboard && e.keyCode === 27 && modal.isTopModal()) { ++ if (keyboard && (e.keyCode === 27 || e.key === 'Escape') && modal.isTopModal()) { + onEscapeKeyDown == null ? void 0 : onEscapeKeyDown(e); + + if (!e.defaultPrevented) { +diff --git a/node_modules/react-overlays/cjs/useRootClose.js b/node_modules/react-overlays/cjs/useRootClose.js +index 3fc40a7..cc16674 100644 +--- a/node_modules/react-overlays/cjs/useRootClose.js ++++ b/node_modules/react-overlays/cjs/useRootClose.js +@@ -66,7 +66,7 @@ function useRootClose(ref, onRootClose, _temp) { + } + }); + var handleKeyUp = (0, _useEventCallback["default"])(function (e) { +- if (e.keyCode === escapeKeyCode) { ++ if (e.keyCode === escapeKeyCode || e.key === 'Escape') { + onClose(e); + } + }); +diff --git a/node_modules/react-overlays/esm/Modal.js b/node_modules/react-overlays/esm/Modal.js +index 42f79e4..a12ebac 100644 +--- a/node_modules/react-overlays/esm/Modal.js ++++ b/node_modules/react-overlays/esm/Modal.js +@@ -193,7 +193,7 @@ var Modal = /*#__PURE__*/forwardRef(function (_ref, ref) { + } + }); + var handleDocumentKeyDown = useEventCallback(function (e) { +- if (keyboard && e.keyCode === 27 && modal.isTopModal()) { ++ if (keyboard && (e.keyCode === 27 || e.key === 'Escape') && modal.isTopModal()) { + onEscapeKeyDown == null ? void 0 : onEscapeKeyDown(e); + + if (!e.defaultPrevented) { +diff --git a/node_modules/react-overlays/esm/useRootClose.js b/node_modules/react-overlays/esm/useRootClose.js +index 8c7882c..30d0d56 100644 +--- a/node_modules/react-overlays/esm/useRootClose.js ++++ b/node_modules/react-overlays/esm/useRootClose.js +@@ -53,7 +53,7 @@ function useRootClose(ref, onRootClose, _temp) { + } + }); + var handleKeyUp = useEventCallback(function (e) { +- if (e.keyCode === escapeKeyCode) { ++ if (e.keyCode === escapeKeyCode || e.key === 'Escape') { + onClose(e); + } + }); diff --git a/src/ButtonGroup.js b/src/ButtonGroup.js index 1ea2a68845..84229f5b17 100644 --- a/src/ButtonGroup.js +++ b/src/ButtonGroup.js @@ -1,7 +1,7 @@ import classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; -import all from 'prop-types-extra/lib/all'; +import warning from 'warning'; import Button from './Button'; import { @@ -19,13 +19,7 @@ const propTypes = { * Display block buttons; only useful when used with the "vertical" prop. * @type {bool} */ - block: all( - PropTypes.bool, - ({ block, vertical }) => - block && !vertical - ? new Error('`block` requires `vertical` to be set to have any effect') - : null - ) + block: PropTypes.bool }; const defaultProps = { @@ -39,6 +33,11 @@ class ButtonGroup extends React.Component { const { block, justified, vertical, className, ...props } = this.props; const [bsProps, elementProps] = splitBsProps(props); + warning( + !(block && !vertical), + '`block` requires `vertical` to be set to have any effect' + ); + const classes = { ...getClassSet(bsProps), [prefix(bsProps)]: !vertical, diff --git a/src/CarouselItem.js b/src/CarouselItem.js index 4c7d0eeda1..e4a11abbf8 100644 --- a/src/CarouselItem.js +++ b/src/CarouselItem.js @@ -1,7 +1,6 @@ import classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; import transition from 'dom-helpers/transition'; const propTypes = { @@ -29,6 +28,7 @@ class CarouselItem extends React.Component { direction: null }; + this.containerRef = React.createRef(); this.isUnmounted = false; } @@ -43,7 +43,7 @@ class CarouselItem extends React.Component { const prevActive = prevProps.active; if (!active && prevActive) { - transition.end(ReactDOM.findDOMNode(this), this.handleAnimateOutEnd); + transition.end(this.containerRef.current, this.handleAnimateOutEnd); } if (active !== prevActive) { @@ -99,7 +99,13 @@ class CarouselItem extends React.Component { classes[this.state.direction] = true; } - return
; + return ( +
+ ); } } diff --git a/src/Collapse.js b/src/Collapse.js index 6d181aeec0..affcc4530b 100644 --- a/src/Collapse.js +++ b/src/Collapse.js @@ -11,6 +11,7 @@ import Transition, { import capitalize from './utils/capitalize'; import createChainedFunction from './utils/createChainedFunction'; +import { getElementRef, makeMergedRef } from './utils/mergeRefs'; const MARGINS = { height: ['marginTop', 'marginBottom'], @@ -134,6 +135,12 @@ const defaultProps = { }; class Collapse extends React.Component { + constructor(props) { + super(props); + + this.childRef = React.createRef(); + } + getDimension() { return typeof this.props.dimension === 'function' ? this.props.dimension() @@ -146,31 +153,34 @@ class Collapse extends React.Component { } /* -- Expanding -- */ - handleEnter = elem => { - elem.style[this.getDimension()] = '0'; + handleEnter = () => { + this.childRef.current.style[this.getDimension()] = '0'; }; - handleEntering = elem => { + handleEntering = () => { const dimension = this.getDimension(); - elem.style[dimension] = this._getScrollDimensionValue(elem, dimension); + this.childRef.current.style[dimension] = this._getScrollDimensionValue( + this.childRef.current, + dimension + ); }; - handleEntered = elem => { - elem.style[this.getDimension()] = null; + handleEntered = () => { + this.childRef.current.style[this.getDimension()] = null; }; /* -- Collapsing -- */ - handleExit = elem => { + handleExit = () => { const dimension = this.getDimension(); - elem.style[dimension] = `${this.props.getDimensionValue( + this.childRef.current.style[dimension] = `${this.props.getDimensionValue( dimension, - elem + this.childRef.current )}px`; - triggerBrowserReflow(elem); + triggerBrowserReflow(this.childRef.current); }; - handleExiting = elem => { - elem.style[this.getDimension()] = '0'; + handleExiting = () => { + this.childRef.current.style[this.getDimension()] = '0'; }; render() { @@ -197,10 +207,13 @@ class Collapse extends React.Component { const handleExit = createChainedFunction(this.handleExit, onExit); const handleExiting = createChainedFunction(this.handleExiting, onExiting); + const ref = makeMergedRef([this.childRef, getElementRef(children)]); + return ( React.cloneElement(children, { ...innerProps, + ref, className: classNames( className, children.props.className, diff --git a/src/ControlLabel.js b/src/ControlLabel.js index 497e51d130..b833daa8dc 100644 --- a/src/ControlLabel.js +++ b/src/ControlLabel.js @@ -3,6 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import warning from 'warning'; +import FormGroupContext from './FormGroupContext'; import { bsClass, getClassSet, splitBsProps } from './utils/bootstrapUtils'; const propTypes = { @@ -17,13 +18,9 @@ const defaultProps = { srOnly: false }; -const contextTypes = { - $bs_formGroup: PropTypes.object -}; - class ControlLabel extends React.Component { render() { - const formGroup = this.context.$bs_formGroup; + const formGroup = this.context; const controlId = formGroup && formGroup.controlId; const { htmlFor = controlId, srOnly, className, ...props } = this.props; @@ -51,6 +48,6 @@ class ControlLabel extends React.Component { ControlLabel.propTypes = propTypes; ControlLabel.defaultProps = defaultProps; -ControlLabel.contextTypes = contextTypes; +ControlLabel.contextType = FormGroupContext; export default bsClass('control-label', ControlLabel); diff --git a/src/Dropdown.js b/src/Dropdown.js index 1fd68d3c4a..7bf64ad97d 100644 --- a/src/Dropdown.js +++ b/src/Dropdown.js @@ -3,19 +3,19 @@ import activeElement from 'dom-helpers/activeElement'; import contains from 'dom-helpers/query/contains'; import React, { cloneElement } from 'react'; import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; -import all from 'prop-types-extra/lib/all'; import elementType from 'prop-types-extra/lib/elementType'; import isRequiredForA11y from 'prop-types-extra/lib/isRequiredForA11y'; import uncontrollable from 'uncontrollable'; +import warning from 'warning'; import ButtonGroup from './ButtonGroup'; import DropdownMenu from './DropdownMenu'; import DropdownToggle from './DropdownToggle'; import { bsClass as setBsClass, prefix } from './utils/bootstrapUtils'; import createChainedFunction from './utils/createChainedFunction'; -import { exclusiveRoles, requiredRoles } from './utils/PropTypes'; +import { getDuplicateRoleError, getMissingRoleError } from './utils/PropTypes'; import ValidComponentChildren from './utils/ValidComponentChildren'; +import { getElementRef, makeMergedRef } from './utils/mergeRefs'; const TOGGLE_ROLE = DropdownToggle.defaultProps.bsRole; const MENU_ROLE = DropdownMenu.defaultProps.bsRole; @@ -41,10 +41,7 @@ const propTypes = { * The children of a Dropdown may be a `` or a ``. * @type {node} */ - children: all( - requiredRoles(TOGGLE_ROLE, MENU_ROLE), - exclusiveRoles(MENU_ROLE) - ), + children: PropTypes.node, /** * Whether or not component is disabled. @@ -123,6 +120,7 @@ class Dropdown extends React.Component { this.handleClose = this.handleClose.bind(this); this._focusInDropdown = false; + this.containerRef = React.createRef(); this.lastOpenEventType = null; } @@ -133,7 +131,7 @@ class Dropdown extends React.Component { UNSAFE_componentWillUpdate(nextProps) { if (!nextProps.open && this.props.open) { this._focusInDropdown = contains( - ReactDOM.findDOMNode(this.menu), + this.containerRef.current.querySelector('[role=menu]'), activeElement(document) ); } @@ -158,7 +156,9 @@ class Dropdown extends React.Component { } focus() { - const toggle = ReactDOM.findDOMNode(this.toggle); + const toggle = this.containerRef.current.querySelector( + '[role=button][aria-haspopup]' + ); if (toggle && toggle.focus) { toggle.focus(); @@ -237,11 +237,12 @@ class Dropdown extends React.Component { } renderMenu(child, { id, onSelect, rootCloseEvent, ...props }) { - let ref = c => { - this.menu = c; - }; - - ref = createChainedFunction(child.ref, ref); + const ref = makeMergedRef([ + el => { + this.menu = el; + }, + getElementRef(child) + ]); return cloneElement(child, { ...props, @@ -259,15 +260,8 @@ class Dropdown extends React.Component { } renderToggle(child, props) { - let ref = c => { - this.toggle = c; - }; - - ref = createChainedFunction(child.ref, ref); - return cloneElement(child, { ...props, - ref, bsClass: prefix(props, 'toggle'), onClick: createChainedFunction(child.props.onClick, this.handleClick), onKeyDown: createChainedFunction( @@ -296,6 +290,25 @@ class Dropdown extends React.Component { delete props.onToggle; + const missingRoleError = getMissingRoleError( + 'Dropdown', + children, + TOGGLE_ROLE, + MENU_ROLE + ); + if (missingRoleError) { + warning(false, missingRoleError); + } + + const duplicateRoleError = getDuplicateRoleError( + 'Dropdown', + children, + MENU_ROLE + ); + if (duplicateRoleError) { + warning(false, duplicateRoleError); + } + const classes = { [bsClass]: true, open, @@ -311,31 +324,33 @@ class Dropdown extends React.Component { // underlying component, to allow it to render size and style variants. return ( - - {ValidComponentChildren.map(children, child => { - switch (child.props.bsRole) { - case TOGGLE_ROLE: - return this.renderToggle(child, { - id, - disabled, - open, - role, - bsClass - }); - case MENU_ROLE: - return this.renderMenu(child, { - id, - open, - pullRight, - bsClass, - onSelect, - rootCloseEvent - }); - default: - return child; - } - })} - +
+ + {ValidComponentChildren.map(children, child => { + switch (child.props.bsRole) { + case TOGGLE_ROLE: + return this.renderToggle(child, { + id, + disabled, + open, + role, + bsClass + }); + case MENU_ROLE: + return this.renderMenu(child, { + id, + open, + pullRight, + bsClass, + onSelect, + rootCloseEvent + }); + default: + return child; + } + })} + +
); } } diff --git a/src/DropdownMenu.js b/src/DropdownMenu.js index 8d4d9fad87..7beaeba0c6 100644 --- a/src/DropdownMenu.js +++ b/src/DropdownMenu.js @@ -1,8 +1,7 @@ import classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; -import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper'; +import { useRootClose } from 'react-overlays'; import { bsClass, @@ -33,10 +32,12 @@ class DropdownMenu extends React.Component { this.handleRootClose = this.handleRootClose.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); + + this.menuRef = React.createRef(); } getFocusableMenuItems() { - const node = ReactDOM.findDOMNode(this); + const node = this.menuRef.current; if (!node) { return []; } @@ -117,8 +118,10 @@ class DropdownMenu extends React.Component { disabled={!open} onRootClose={this.handleRootClose} event={rootCloseEvent} + menuRef={this.menuRef} >