diff --git a/.yarn/offline-mirror/lodash.findlast-4.6.0.tgz b/.yarn/offline-mirror/lodash.findlast-4.6.0.tgz new file mode 100644 index 000000000000..8d01a4a89b59 Binary files /dev/null and b/.yarn/offline-mirror/lodash.findlast-4.6.0.tgz differ diff --git a/packages/react/.storybook/Container.js b/packages/react/.storybook/Container.js index af9516cc7119..31ebc439da1e 100644 --- a/packages/react/.storybook/Container.js +++ b/packages/react/.storybook/Container.js @@ -37,11 +37,6 @@ function Container({ story }) { }}> {story()} - ); } diff --git a/packages/react/package.json b/packages/react/package.json index 66ca2f4be944..60b6c1e93226 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -42,9 +42,9 @@ "classnames": "2.2.6", "downshift": "^1.31.14", "flatpickr": "4.6.1", - "focus-trap-react": "^6.0.0", "invariant": "^2.2.3", "lodash.debounce": "^4.0.8", + "lodash.findlast": "^4.5.0", "lodash.isequal": "^4.5.0", "lodash.omit": "^4.5.0", "react-is": "^16.8.6", diff --git a/packages/react/src/components/ComposedModal/ComposedModal.js b/packages/react/src/components/ComposedModal/ComposedModal.js index bee7b6083a83..72d646c6428e 100644 --- a/packages/react/src/components/ComposedModal/ComposedModal.js +++ b/packages/react/src/components/ComposedModal/ComposedModal.js @@ -13,6 +13,7 @@ import { settings } from 'carbon-components'; import { Close20 } from '@carbon/icons-react'; import toggleClass from '../../tools/toggleClass'; import requiredIfGivenPropExists from '../../prop-types/requiredIfGivenPropExists'; +import wrapFocus from '../../internal/wrapFocus'; const { prefix } = settings; @@ -27,6 +28,8 @@ export default class ComposedModal extends Component { outerModal = React.createRef(); innerModal = React.createRef(); button = React.createRef(); + startSentinel = React.createRef(); + endSentinel = React.createRef(); static propTypes = { /** @@ -78,19 +81,6 @@ export default class ComposedModal extends Component { }; } - elementOrParentIsFloatingMenu = target => { - const { - selectorsFloatingMenus = [ - `.${prefix}--overflow-menu-options`, - `.${prefix}--tooltip`, - '.flatpickr-calendar', - ], - } = this.props; - if (target && typeof target.closest === 'function') { - return selectorsFloatingMenus.some(selector => target.closest(selector)); - } - }; - handleKeyDown = evt => { // Esc key if (evt.which === 27) { @@ -109,22 +99,23 @@ export default class ComposedModal extends Component { } }; - focusModal = () => { - if (this.outerModal.current) { - this.outerModal.current.focus(); - } - }; - - handleBlur = evt => { - // Keyboard trap - if ( - this.innerModal.current && - this.props.open && - evt.relatedTarget && - !this.innerModal.current.contains(evt.relatedTarget) && - !this.elementOrParentIsFloatingMenu(evt.relatedTarget) - ) { - this.focusModal(); + handleBlur = ({ + target: oldActiveNode, + relatedTarget: currentActiveNode, + }) => { + const { open, selectorsFloatingMenus } = this.props; + if (open && currentActiveNode && oldActiveNode) { + const { current: modalNode } = this.innerModal; + const { current: startSentinelNode } = this.startSentinel; + const { current: endSentinelNode } = this.endSentinel; + wrapFocus({ + modalNode, + startSentinelNode, + endSentinelNode, + currentActiveNode, + oldActiveNode, + selectorsFloatingMenus, + }); } }; @@ -240,11 +231,26 @@ export default class ComposedModal extends Component { onClick={this.handleClick} onKeyDown={this.handleKeyDown} onTransitionEnd={open ? this.handleTransitionEnd : undefined} - className={modalClass} - tabIndex={-1}> -
+ className={modalClass}> + {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */} + + Focus sentinel + +
{childrenWithProps}
+ {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */} + + Focus sentinel +
); } diff --git a/packages/react/src/components/ComposedModal/__snapshots__/ComposedModal-test.js.snap b/packages/react/src/components/ComposedModal/__snapshots__/ComposedModal-test.js.snap index 3ea2f12e1c4b..0eaefdfd2816 100644 --- a/packages/react/src/components/ComposedModal/__snapshots__/ComposedModal-test.js.snap +++ b/packages/react/src/components/ComposedModal/__snapshots__/ComposedModal-test.js.snap @@ -14,11 +14,25 @@ exports[` renders 1`] = ` onTransitionEnd={[Function]} open={true} role="presentation" - tabIndex={-1} > + + Focus sentinel +
+ + Focus sentinel +
`; diff --git a/packages/react/src/components/Modal/Modal-story.js b/packages/react/src/components/Modal/Modal-story.js index eb2ffaee4133..ef34780ed8a5 100644 --- a/packages/react/src/components/Modal/Modal-story.js +++ b/packages/react/src/components/Modal/Modal-story.js @@ -32,7 +32,6 @@ const props = () => ({ 'Enter key to submit (shouldSubmitOnEnter)', false ), - focusTrap: boolean('Trap focus (focusTrap)', false), hasScrollingContent: boolean( 'Modal contains scrollable content (hasScrollingContent)', false diff --git a/packages/react/src/components/Modal/Modal-test.js b/packages/react/src/components/Modal/Modal-test.js index f45be8aea838..4afcb7a8ffad 100644 --- a/packages/react/src/components/Modal/Modal-test.js +++ b/packages/react/src/components/Modal/Modal-test.js @@ -15,7 +15,7 @@ import { settings } from 'carbon-components'; const { prefix } = settings; // The modal is the 0th child inside the wrapper on account of focus-trap-react -const getModal = wrapper => wrapper.childAt(0); +const getModal = wrapper => wrapper.find('.bx--modal'); describe('Modal', () => { describe('Renders as expected', () => { diff --git a/packages/react/src/components/Modal/Modal.js b/packages/react/src/components/Modal/Modal.js index db00762727f6..e97aba523ead 100644 --- a/packages/react/src/components/Modal/Modal.js +++ b/packages/react/src/components/Modal/Modal.js @@ -10,10 +10,13 @@ import React, { Component } from 'react'; import classNames from 'classnames'; import { settings } from 'carbon-components'; import { Close20 } from '@carbon/icons-react'; -import FocusTrap from 'focus-trap-react'; import toggleClass from '../../tools/toggleClass'; import Button from '../Button'; +import deprecate from '../../prop-types/deprecate'; import requiredIfGivenPropExists from '../../prop-types/requiredIfGivenPropExists'; +import wrapFocus, { + elementOrParentIsFloatingMenu, +} from '../../internal/wrapFocus'; import setupGetInstanceId from '../../tools/setupGetInstanceId'; const { prefix } = settings; @@ -138,10 +141,13 @@ export default class Modal extends Component { size: PropTypes.oneOf(['xs', 'sm', 'lg']), /** - * Specify whether the modal should use 3rd party `focus-trap-react` for the focus-wrap feature. - * NOTE: by default this is true. + * Deprecated; Used for advanced focus-wrapping feature using 3rd party library, + * but it's now achieved without a 3rd party library. */ - focusTrap: PropTypes.bool, + focusTrap: deprecate( + PropTypes.bool, + `\nThe prop \`focusTrap\` for Modal has been deprecated, as the feature of \`focusTrap\` runs by default.` + ), /** * Specify whether the modal contains scrolling content @@ -167,30 +173,18 @@ export default class Modal extends Component { modalHeading: '', modalLabel: '', selectorPrimaryFocus: '[data-modal-primary-focus]', - focusTrap: true, hasScrollingContent: false, }; button = React.createRef(); outerModal = React.createRef(); innerModal = React.createRef(); + startTrap = React.createRef(); + endTrap = React.createRef(); modalInstanceId = `modal-${getInstanceId()}`; modalLabelId = `${prefix}--modal-header__label--${this.modalInstanceId}`; modalHeadingId = `${prefix}--modal-header__heading--${this.modalInstanceId}`; - elementOrParentIsFloatingMenu = target => { - const { - selectorsFloatingMenus = [ - `.${prefix}--overflow-menu-options`, - `.${prefix}--tooltip`, - '.flatpickr-calendar', - ], - } = this.props; - if (target && typeof target.closest === 'function') { - return selectorsFloatingMenus.some(selector => target.closest(selector)); - } - }; - handleKeyDown = evt => { if (this.props.open) { if (evt.which === 27) { @@ -206,28 +200,32 @@ export default class Modal extends Component { if ( this.innerModal.current && !this.innerModal.current.contains(evt.target) && - !this.elementOrParentIsFloatingMenu(evt.target) + !elementOrParentIsFloatingMenu( + evt.target, + this.props.selectorsFloatingMenus + ) ) { this.props.onRequestClose(evt); } }; - focusModal = () => { - if (this.outerModal.current) { - this.outerModal.current.focus(); - } - }; - - handleBlur = evt => { - // Keyboard trap - if ( - this.innerModal.current && - this.props.open && - evt.relatedTarget && - !this.innerModal.current.contains(evt.relatedTarget) && - !this.elementOrParentIsFloatingMenu(evt.relatedTarget) - ) { - this.focusModal(); + handleBlur = ({ + target: oldActiveNode, + relatedTarget: currentActiveNode, + }) => { + const { open, selectorsFloatingMenus } = this.props; + if (open && currentActiveNode && oldActiveNode) { + const { current: modalNode } = this.innerModal; + const { current: startTrapNode } = this.startTrap; + const { current: endTrapNode } = this.endTrap; + wrapFocus({ + modalNode, + startTrapNode, + endTrapNode, + currentActiveNode, + oldActiveNode, + selectorsFloatingMenus, + }); } }; @@ -277,9 +275,7 @@ export default class Modal extends Component { if (!this.props.open) { return; } - if (!this.props.focusTrap) { - this.focusButton(this.innerModal.current); - } + this.focusButton(this.innerModal.current); } handleTransitionEnd = evt => { @@ -290,9 +286,7 @@ export default class Modal extends Component { this.outerModal.current.offsetHeight && this.beingOpen ) { - if (!this.props.focusTrap) { - this.focusButton(evt.currentTarget); - } + this.focusButton(evt.currentTarget); this.beingOpen = false; } }; @@ -317,7 +311,6 @@ export default class Modal extends Component { selectorsFloatingMenus, // eslint-disable-line shouldSubmitOnEnter, // eslint-disable-line size, - focusTrap, hasScrollingContent, ...other } = this.props; @@ -379,7 +372,8 @@ export default class Modal extends Component { role="dialog" className={containerClasses} aria-label={ariaLabel} - aria-modal="true"> + aria-modal="true" + tabIndex="-1">
{passiveModal && modalButton} {modalLabel && ( @@ -422,7 +416,7 @@ export default class Modal extends Component {
); - const modal = ( + return (
+ {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */} + + Focus sentinel + {modalBody} + {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */} + + Focus sentinel +
); - - return !focusTrap ? ( - modal - ) : ( - // `` has `active: true` in its `defaultProps` - - {modal} - - ); } } diff --git a/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap b/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap index 43e31cfd247e..26cbea0e21c7 100644 --- a/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap +++ b/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap @@ -43,7 +43,6 @@ exports[`ModalWrapper should render 1`] = ` - + + Focus sentinel + - + + Focus sentinel + + diff --git a/packages/react/src/internal/FloatingMenu.js b/packages/react/src/internal/FloatingMenu.js index 2e71a9ab5be2..c5d5ab9776af 100644 --- a/packages/react/src/internal/FloatingMenu.js +++ b/packages/react/src/internal/FloatingMenu.js @@ -10,6 +10,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; import window from 'window-or-global'; +import { settings } from 'carbon-components'; + +const { prefix } = settings; /** * The structure for the position of floating menu. @@ -333,7 +336,25 @@ class FloatingMenu extends React.Component { if (typeof document !== 'undefined') { const { target } = this.props; return ReactDOM.createPortal( - this._getChildrenWithProps(), + <> + {/* Non-translatable: Focus management code makes this `` not actually read by screen readers */} + + Focus sentinel + + {this._getChildrenWithProps()} + {/* Non-translatable: Focus management code makes this `` not actually read by screen readers */} + + Focus sentinel + + , !target ? document.body : target() ); } diff --git a/packages/react/src/internal/__tests__/wrapFocus-test.js b/packages/react/src/internal/__tests__/wrapFocus-test.js new file mode 100644 index 000000000000..646f0e1297ed --- /dev/null +++ b/packages/react/src/internal/__tests__/wrapFocus-test.js @@ -0,0 +1,150 @@ +import wrapFocus from '../wrapFocus'; + +/** + * Copyright IBM Corp. 2020 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +describe('wrapFocus', () => { + let node; + let spyInnerModal; + let spyButton0; + let spyButton2; + + beforeEach(() => { + node = document.createElement('div'); + node.tabIndex = '-1'; + node.innerHTML = ` + + + +
+ + + +
+ + + +
+ `; + document.body.appendChild(node); + spyInnerModal = jest.spyOn(node.querySelector('#inner-modal'), 'focus'); + spyButton0 = jest.spyOn(node.querySelector('#button-0'), 'focus'); + spyButton2 = jest.spyOn(node.querySelector('#button-2'), 'focus'); + }); + + it('runs forward focus-wrap when following outer node is focused on', () => { + wrapFocus({ + modalNode: node.querySelector('#inner-modal'), + startSentinelNode: node.querySelector('#start-sentinel'), + endSentinelNode: node.querySelector('#end-sentinel'), + currentActiveNode: node.querySelector('#outer-following'), + oldActiveNode: node.querySelector('#button-2'), + }); + expect(spyButton0).toHaveBeenCalled(); + }); + + it('runs forward focus-wrap when following focus sentinel is focused on', () => { + wrapFocus({ + modalNode: node.querySelector('#inner-modal'), + startSentinelNode: node.querySelector('#start-sentinel'), + endSentinelNode: node.querySelector('#end-sentinel'), + currentActiveNode: node.querySelector('#end-sentinel'), + oldActiveNode: node.querySelector('#button-2'), + }); + expect(spyButton0).toHaveBeenCalled(); + }); + + it('runs reverse focus-wrap when preceding outer node is focused on', () => { + wrapFocus({ + modalNode: node.querySelector('#inner-modal'), + startSentinelNode: node.querySelector('#start-sentinel'), + endSentinelNode: node.querySelector('#end-sentinel'), + currentActiveNode: node.querySelector('#outer-preceding'), + oldActiveNode: node.querySelector('#button-0'), + }); + expect(spyButton2).toHaveBeenCalled(); + }); + + it('runs reverse focus-wrap when preceding focus sentinel is focused on', () => { + wrapFocus({ + modalNode: node.querySelector('#inner-modal'), + startSentinelNode: node.querySelector('#start-sentinel'), + endSentinelNode: node.querySelector('#end-sentinel'), + currentActiveNode: node.querySelector('#start-sentinel'), + oldActiveNode: node.querySelector('#button-0'), + }); + expect(spyButton2).toHaveBeenCalled(); + }); + + it('does not run focus-wrap when a floating menu is focused on', () => { + wrapFocus({ + modalNode: node.querySelector('#inner-modal'), + startSentinelNode: node.querySelector('#start-sentinel'), + endSentinelNode: node.querySelector('#end-sentinel'), + currentActiveNode: node.querySelector('.bx--tooltip'), + oldActiveNode: node.querySelector('#button-2'), + }); + expect(spyInnerModal).not.toHaveBeenCalled(); + expect(spyButton0).not.toHaveBeenCalled(); + expect(spyButton2).not.toHaveBeenCalled(); + }); + + it('uses inner modal node as a escape hatch for focusing for forward focus-wrap', () => { + node.querySelector( + '#inner-modal' + ).innerHTML = `
`; + wrapFocus({ + modalNode: node.querySelector('#inner-modal'), + startSentinelNode: node.querySelector('#start-sentinel'), + endSentinelNode: node.querySelector('#end-sentinel'), + currentActiveNode: node.querySelector('#outer-following'), + oldActiveNode: node.querySelector('#dummy-old-active-node'), + }); + expect(spyInnerModal).toHaveBeenCalled(); + }); + + it('uses inner modal node as a escape hatch for focusing for reverse focus-wrap', () => { + node.querySelector( + '#inner-modal' + ).innerHTML = `
`; + wrapFocus({ + modalNode: node.querySelector('#inner-modal'), + startSentinelNode: node.querySelector('#start-sentinel'), + endSentinelNode: node.querySelector('#end-sentinel'), + currentActiveNode: node.querySelector('#outer-preceding'), + oldActiveNode: node.querySelector('#dummy-old-active-node'), + }); + expect(spyInnerModal).toHaveBeenCalled(); + }); + + afterEach(() => { + if (spyButton2) { + spyButton2.mockRestore(); + spyButton2 = null; + } + if (spyButton0) { + spyButton0.mockRestore(); + spyButton0 = null; + } + if (spyInnerModal) { + spyInnerModal.mockRestore(); + spyInnerModal = null; + } + if (node) { + node.parentNode.removeChild(node); + node = null; + } + }); +}); diff --git a/packages/react/src/internal/keyboard/navigation.js b/packages/react/src/internal/keyboard/navigation.js index 14d98c44969f..cdd85de640f4 100644 --- a/packages/react/src/internal/keyboard/navigation.js +++ b/packages/react/src/internal/keyboard/navigation.js @@ -24,7 +24,7 @@ import { match } from './match'; * getNextIndex(keyCodes.RIGHT, 0, 4) */ -const getNextIndex = (key, index, arrayLength) => { +export const getNextIndex = (key, index, arrayLength) => { if (match(key, ArrowRight)) { return (index + 1) % arrayLength; } @@ -33,4 +33,30 @@ const getNextIndex = (key, index, arrayLength) => { } }; -export { getNextIndex }; +/** + * A flag `node.compareDocumentPosition(target)` returns, + * that indicates `target` is located earlier than `node` in the document or `target` contains `node`. + */ +export const DOCUMENT_POSITION_BROAD_PRECEDING = + // Checks `typeof Node` for `react-docgen` + typeof Node !== 'undefined' && + Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS; + +/** + * A flag `node.compareDocumentPosition(target)` returns, + * that indicates `target` is located later than `node` in the document or `node` contains `target`. + */ +export const DOCUMENT_POSITION_BROAD_FOLLOWING = + // Checks `typeof Node` for `react-docgen` + typeof Node !== 'undefined' && + Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY; + +/** + * CSS selector that selects major nodes that is sequential-focusable. + */ +export const selectorTabbable = ` + a[href], area[href], input:not([disabled]):not([tabindex='-1']), + button:not([disabled]):not([tabindex='-1']),select:not([disabled]):not([tabindex='-1']), + textarea:not([disabled]):not([tabindex='-1']), + iframe, object, embed, *[tabindex]:not([tabindex='-1']), *[contenteditable=true] +`; diff --git a/packages/react/src/internal/wrapFocus.js b/packages/react/src/internal/wrapFocus.js new file mode 100644 index 000000000000..27463d0b36ad --- /dev/null +++ b/packages/react/src/internal/wrapFocus.js @@ -0,0 +1,95 @@ +/** + * Copyright IBM Corp. 2020 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import findLast from 'lodash.findlast'; +import { settings } from 'carbon-components'; +import { + DOCUMENT_POSITION_BROAD_PRECEDING, + DOCUMENT_POSITION_BROAD_FOLLOWING, + selectorTabbable, +} from './keyboard/navigation'; + +const { prefix } = settings; + +/** + * @param {Node} node A DOM node. + * @param {string[]} selectorsFloatingMenus The CSS selectors that matches floating menus. + * @returns {boolean} `true` of the given `node` is in a floating menu. + */ +function elementOrParentIsFloatingMenu( + node, + selectorsFloatingMenus = [ + `.${prefix}--overflow-menu-options`, + `.${prefix}--tooltip`, + '.flatpickr-calendar', + ] +) { + if (node && typeof node.closest === 'function') { + return selectorsFloatingMenus.some(selector => node.closest(selector)); + } +} + +/** + * Ensures the focus is kept in the given `modalNode`, implementing "focus-wrap" behavior. + * @param {object} options The options. + * @param {Node} options.modalNode The DOM node of the inner modal. + * @param {Node} options.startTrapNode The DOM node of the focus sentinel the is placed earlier next to `modalNode`. + * @param {Node} options.endTrapNode The DOM node of the focus sentinel the is placed next to `modalNode`. + * @param {Node} options.currentActiveNode The DOM node that has focus. + * @param {Node} options.oldActiveNode The DOM node that previously had focus. + * @param {Node} [options.selectorsFloatingMenus] The CSS selectors that matches floating menus. + */ +function wrapFocus({ + modalNode, + startTrapNode, + endTrapNode, + currentActiveNode, + oldActiveNode, + selectorsFloatingMenus, +}) { + if ( + modalNode && + currentActiveNode && + oldActiveNode && + !modalNode.contains(currentActiveNode) && + !elementOrParentIsFloatingMenu(currentActiveNode, selectorsFloatingMenus) + ) { + const comparisonResult = oldActiveNode.compareDocumentPosition( + currentActiveNode + ); + if ( + currentActiveNode === startTrapNode || + comparisonResult & DOCUMENT_POSITION_BROAD_PRECEDING + ) { + const tabbable = findLast( + modalNode.querySelectorAll(selectorTabbable), + elem => Boolean(elem.offsetParent) + ); + if (tabbable) { + tabbable.focus(); + } else if (modalNode !== oldActiveNode) { + modalNode.focus(); + } + } else if ( + currentActiveNode === endTrapNode || + comparisonResult & DOCUMENT_POSITION_BROAD_FOLLOWING + ) { + const tabbable = Array.prototype.find.call( + modalNode.querySelectorAll(selectorTabbable), + elem => Boolean(elem.offsetParent) + ); + if (tabbable) { + tabbable.focus(); + } else if (modalNode !== oldActiveNode) { + modalNode.focus(); + } + } + } +} + +export { elementOrParentIsFloatingMenu }; +export default wrapFocus; diff --git a/yarn.lock b/yarn.lock index 5dba2fc33c13..e560b5eee5cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14465,6 +14465,11 @@ lodash.escaperegexp@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= +lodash.findlast@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.findlast/-/lodash.findlast-4.6.0.tgz#ea8bb78cf2e7e7804fc8aeb7d1953e07fe31fbc8" + integrity sha1-6ou3jPLn54BPyK630ZU+B/4x+8g= + lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" @@ -19716,14 +19721,7 @@ rollup-pluginutils@2.0.1: estree-walker "^0.3.0" micromatch "^2.3.11" -rollup-pluginutils@2.8.2: - version "2.8.2" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" - integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== - dependencies: - estree-walker "^0.6.1" - -rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1: +rollup-pluginutils@2.8.2, rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1: version "2.8.2" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==