diff --git a/.flowconfig b/.flowconfig index 8a4b98e9ae..f494ab7c0c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -29,6 +29,7 @@ sharedmemory.hash_table_pow=21 esproposal.export_star_as=enable esproposal.optional_chaining=enable module.file_ext=.js +module.file_ext=.cjs module.file_ext=.scss module.name_mapper.extension='scss' -> '/flow/EmptyFlowStub.js.flow' module.name_mapper.extension='css' -> '/flow/EmptyFlowStub.js.flow' diff --git a/package.json b/package.json index 34bcc189e9..5bcb22a7b7 100644 --- a/package.json +++ b/package.json @@ -266,7 +266,7 @@ "react-responsive": "^10.0.0", "react-router-dom": "^5.3.4", "react-scrollbars-custom": "^4.0.21", - "react-tether": "^1.0.5", + "react-tether": "^3.0.3", "react-textarea-autosize": "^8.5.3", "regenerator-runtime": "^0.14.1", "remarkable": "^2.0.1", @@ -332,11 +332,11 @@ "mousetrap": "^1.6.3", "pikaday": "^1.8.0", "query-string": "5.1.1", - "react": "^17.0.1 || ^18.0.0", + "react": "^18.0.0 || ^19.0.0", "react-animate-height": "^3.2.3", "react-aria-components": "^1.10.1", "react-beautiful-dnd": "^13.1.1", - "react-dom": "^17.0.1 || ^18.0.0", + "react-dom": "^18.0.0 || ^19.0.0", "react-draggable": "^4.5.0", "react-immutable-proptypes": "^2.1.0", "react-intl": ">=2.9.0", @@ -347,7 +347,7 @@ "react-responsive": "^10.0.0", "react-router-dom": "5.3.4", "react-scrollbars-custom": "^4.0.21", - "react-tether": "^1.0.5", + "react-tether": "^3.0.3", "react-textarea-autosize": "^8.5.3", "regenerator-runtime": "^0.13.2", "remarkable": "^2.0.1", @@ -357,11 +357,6 @@ "tabbable": "^1.1.2", "uuid": "^8.3.2" }, - "comments": { - "dependencies": { - "react-tether": "Version 2.x has too many breaking changes and requires forwardRef on all components" - } - }, "msw": { "workerDirectory": [".storybook/public"] } diff --git a/src/components/button-group/ButtonGroup.scss b/src/components/button-group/ButtonGroup.scss index 94d71c2aea..a552d0e3b4 100644 --- a/src/components/button-group/ButtonGroup.scss +++ b/src/components/button-group/ButtonGroup.scss @@ -43,7 +43,8 @@ &, & > .bdl-targeted-click-through { - > .btn { + > .btn, + > .bdl-Tooltip-target > .btn { margin: 5px 0 5px -1px; border-radius: 0; @@ -64,17 +65,20 @@ } } - > .btn:first-child { + > .btn:first-child, + > .bdl-Tooltip-target:first-child > .btn { border-top-left-radius: $bdl-border-radius-size-med; border-bottom-left-radius: $bdl-border-radius-size-med; } - > .btn:last-child { + > .btn:last-child, + > .bdl-Tooltip-target:last-child > .btn { border-top-right-radius: $bdl-border-radius-size-med; border-bottom-right-radius: $bdl-border-radius-size-med; } - > .btn.is-selected { + > .btn.is-selected, + > .bdl-Tooltip-target > .btn.is-selected { z-index: 2; /* place on top of siblings */ color: $bdl-gray-80; background-color: $bdl-gray-10; @@ -82,7 +86,8 @@ box-shadow: none; } - > .btn:focus { + > .btn:focus, + > .bdl-Tooltip-target > .btn:focus { z-index: 3; /* place on top of all other buttons for accessibility */ } } @@ -98,7 +103,7 @@ border: 1px solid $bdl-gray-30; box-shadow: none; cursor: default; - opacity: .4; + opacity: 0.4; } > .btn-primary { diff --git a/src/components/checkbox/Checkbox.scss b/src/components/checkbox/Checkbox.scss index 733ff86c37..1fa3bc63ae 100644 --- a/src/components/checkbox/Checkbox.scss +++ b/src/components/checkbox/Checkbox.scss @@ -75,6 +75,7 @@ .checkbox-tooltip-wrapper { display: inline-flex; vertical-align: text-bottom; + line-height: 0.1; // This keeps the tooltip wrapper height consistent with the child element > .info-tooltip { position: relative; diff --git a/src/components/checkbox/CheckboxTooltip.tsx b/src/components/checkbox/CheckboxTooltip.tsx index defd1038c2..78b7de6969 100644 --- a/src/components/checkbox/CheckboxTooltip.tsx +++ b/src/components/checkbox/CheckboxTooltip.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { defineMessages, FormattedMessage } from 'react-intl'; +import { Focusable, Tooltip, TooltipProvider } from '@box/blueprint-web'; + import IconInfo from '../../icons/general/IconInfo'; -import Tooltip from '../tooltip'; const messages = defineMessages({ checkboxTooltipIconInfoText: { @@ -18,15 +19,19 @@ export interface CheckboxTooltipProps { const CheckboxTooltip = ({ tooltip }: CheckboxTooltipProps) => (
- -
- } - width={16} - /> -
-
+ + + +
+ } + width={16} + /> +
+
+
+
); diff --git a/src/components/context-menu/ContextMenu.tsx b/src/components/context-menu/ContextMenu.tsx index e645b4f9b6..de0b259100 100644 --- a/src/components/context-menu/ContextMenu.tsx +++ b/src/components/context-menu/ContextMenu.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import TetherComponent from 'react-tether'; +import TetherComponent, { TetherProps } from 'react-tether'; import uniqueId from 'lodash/uniqueId'; import './ContextMenu.scss'; @@ -158,20 +158,29 @@ class ContextMenu extends React.Component { onClose: this.handleMenuClose, }; - // TypeScript defs don't work for older versions of react-tether - const tetherProps = { + const tetherProps: TetherProps = { attachment: 'top left', classPrefix: 'context-menu', constraints, targetAttachment: 'top left', targetOffset, + renderElementTo: document.body, }; return ( - - {React.isValidElement(menuTarget) ? React.cloneElement(menuTarget, menuTargetProps) : null} - {isOpen && React.isValidElement(menu) ? React.cloneElement(menu, menuProps) : null} - + { + return React.isValidElement(menuTarget) ? ( +
{React.cloneElement(menuTarget, menuTargetProps)}
+ ) : null; + }} + renderElement={ref => { + return isOpen && React.isValidElement(menu) ? ( +
{React.cloneElement(menu, menuProps)}
+ ) : null; + }} + /> ); } } diff --git a/src/components/context-menu/__tests__/ContextMenu.test.tsx b/src/components/context-menu/__tests__/ContextMenu.test.tsx index 086834e8ba..af3d743c9b 100644 --- a/src/components/context-menu/__tests__/ContextMenu.test.tsx +++ b/src/components/context-menu/__tests__/ContextMenu.test.tsx @@ -2,6 +2,7 @@ // @ts-ignore import React, { act } from 'react'; import { mount, shallow, ReactWrapper } from 'enzyme'; +import TetherComponent from 'react-tether'; import sinon from 'sinon'; import ContextMenu, { ContextMenuProps, ContextMenuState } from '../ContextMenu'; @@ -53,7 +54,7 @@ describe('components/context-menu/ContextMenu', () => { }); test('should correctly render a single child button with correct props', () => { - const wrapper = shallow( + const wrapper = mount( @@ -69,7 +70,7 @@ describe('components/context-menu/ContextMenu', () => { }); test('should not render child menu when menu is closed', () => { - const wrapper = shallow( + const wrapper = mount( @@ -81,13 +82,16 @@ describe('components/context-menu/ContextMenu', () => { }); test('should correctly render a single child menu with correct props when menu is open', () => { - const wrapper = shallow( + const wrapper = mount( , ); - wrapper.setState({ isOpen: true }); + act(() => { + wrapper.setState({ isOpen: true }); + }); + wrapper.update(); const instance = wrapper.instance(); @@ -100,17 +104,18 @@ describe('components/context-menu/ContextMenu', () => { }); test('should render TetherComponent with correct props with correct default values', () => { - const wrapper = shallow( + const wrapper = mount( , ); - expect(wrapper.is('TetherComponent')).toBe(true); - expect(wrapper.prop('attachment')).toEqual('top left'); - expect(wrapper.prop('targetAttachment')).toEqual('top left'); - expect(wrapper.prop('constraints')).toEqual([]); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.length).toBe(1); + expect(tetherComponent.prop('attachment')).toEqual('top left'); + expect(tetherComponent.prop('targetAttachment')).toEqual('top left'); + expect(tetherComponent.prop('constraints')).toEqual([]); }); test('should render TetherComponent with constraints when specified', () => { @@ -153,12 +158,9 @@ describe('components/context-menu/ContextMenu', () => { , ); const instance = wrapper.instance(); - sandbox - .mock(instance) - .expects('setState') - .withArgs({ - isOpen: false, - }); + sandbox.mock(instance).expects('setState').withArgs({ + isOpen: false, + }); instance.closeMenu(); }); @@ -247,7 +249,9 @@ describe('components/context-menu/ContextMenu', () => { const instance = wrapper.instance(); document.addEventListener = jest.fn(); document.removeEventListener = jest.fn(); - instance.setState({ isOpen: true }); + act(() => { + instance.setState({ isOpen: true }); + }); expect(document.addEventListener).not.toHaveBeenCalledWith('click', expect.anything(), expect.anything()); expect(document.addEventListener).not.toHaveBeenCalledWith( 'contextmenu', @@ -290,14 +294,8 @@ describe('components/context-menu/ContextMenu', () => { ); const documentMock = sandbox.mock(document); - documentMock - .expects('removeEventListener') - .withArgs('contextmenu') - .never(); - documentMock - .expects('removeEventListener') - .withArgs('click') - .never(); + documentMock.expects('removeEventListener').withArgs('contextmenu').never(); + documentMock.expects('removeEventListener').withArgs('click').never(); wrapper.unmount(); }); @@ -411,18 +409,18 @@ describe('components/context-menu/ContextMenu', () => { const instance = wrapper.instance() as ContextMenu; instance.closeMenu = closeMenuSpy; - const handleContextMenuEvent = ({ + const handleContextMenuEvent = { clientX: 10, clientY: 15, preventDefault: preventDefaultSpy, - } as unknown) as MouseEvent; + } as unknown as MouseEvent; act(() => { instance.handleContextMenu(handleContextMenuEvent); }); - const documentClickEvent = ({ + const documentClickEvent = { target: document.createElement('div'), - } as unknown) as MouseEvent; + } as unknown as MouseEvent; instance.handleDocumentClick(documentClickEvent); expect(closeMenuSpy).toHaveBeenCalled(); }); @@ -438,18 +436,18 @@ describe('components/context-menu/ContextMenu', () => { const instance = wrapper.instance() as ContextMenu; instance.closeMenu = closeMenuSpy; - const handleContextMenuEvent = ({ + const handleContextMenuEvent = { clientX: 10, clientY: 15, preventDefault: preventDefaultSpy, - } as unknown) as MouseEvent; + } as unknown as MouseEvent; act(() => { instance.handleContextMenu(handleContextMenuEvent); }); - const documentClickEvent = ({ + const documentClickEvent = { target: document.getElementById(instance.menuID), - } as unknown) as MouseEvent; + } as unknown as MouseEvent; instance.handleDocumentClick(documentClickEvent); expect(closeMenuSpy).not.toHaveBeenCalled(); }); diff --git a/src/components/draft-js-editor/DraftJSEditor.js b/src/components/draft-js-editor/DraftJSEditor.js index d34d7347a6..6d9aa7155b 100644 --- a/src/components/draft-js-editor/DraftJSEditor.js +++ b/src/components/draft-js-editor/DraftJSEditor.js @@ -8,7 +8,7 @@ import { Editor } from 'draft-js'; import type { EditorState } from 'draft-js'; import 'draft-js/dist/Draft.css'; -import Tooltip from '../tooltip'; +import { Tooltip, TooltipProvider } from '@box/blueprint-web'; import commonMessages from '../../common/messages'; import './DraftJSEditor.scss'; @@ -126,24 +126,31 @@ class DraftJSEditor extends React.Component { {description} - - {/* need div so tooltip can set aria-describedby */} -
- -
-
+ + +
+ +
+
+
); } diff --git a/src/components/dropdown-menu/DropdownMenu.js b/src/components/dropdown-menu/DropdownMenu.js index 68d152edd9..df4e4f587b 100644 --- a/src/components/dropdown-menu/DropdownMenu.js +++ b/src/components/dropdown-menu/DropdownMenu.js @@ -27,6 +27,8 @@ type Props = { onMenuClose?: (event: SyntheticEvent<> | MouseEvent) => void, /** Handler for dropdown menu open events */ onMenuOpen?: () => void, + /** Optional class name for the target wrapper element */ + targetWrapperClassName?: string, /** "attachment" prop for the TetherComponent, will overwrite the default settings and ignore isRightAligned option */ tetherAttachment?: string, /** "targetAttachment" prop for the TetherComponent, will overwrite the default settings and ignore isRightAligned option */ @@ -187,6 +189,7 @@ class DropdownMenu extends React.Component { constrainToWindowWithPin, isResponsive, isRightAligned, + targetWrapperClassName, tetherAttachment, tetherTargetAttachment, } = this.props; @@ -264,16 +267,25 @@ class DropdownMenu extends React.Component { return ( - {React.cloneElement(menuButton, menuButtonProps)} - {isOpen && React.cloneElement(menu, menuProps)} - + renderTarget={ref => ( +
+ {React.cloneElement(menuButton, menuButtonProps)} +
+ )} + renderElement={ref => { + return isOpen ? ( +
+ {React.cloneElement(menu, menuProps)} +
+ ) : null; + }} + /> ); } } diff --git a/src/components/dropdown-menu/DropdownMenu.scss b/src/components/dropdown-menu/DropdownMenu.scss index e9ac1a327b..926370d557 100644 --- a/src/components/dropdown-menu/DropdownMenu.scss +++ b/src/components/dropdown-menu/DropdownMenu.scss @@ -19,6 +19,10 @@ margin-top: 5px; } +.bdl-DropdownMenu-target { + display: inline-block; +} + @include breakpoint($medium-screen) { .bdl-DropdownMenu--responsive { &.dropdown-menu-enabled { diff --git a/src/components/dropdown-menu/__tests__/DropdownMenu.test.js b/src/components/dropdown-menu/__tests__/DropdownMenu.test.js index 44d9159579..489943f05c 100644 --- a/src/components/dropdown-menu/__tests__/DropdownMenu.test.js +++ b/src/components/dropdown-menu/__tests__/DropdownMenu.test.js @@ -1,5 +1,6 @@ import React, { act } from 'react'; import { mount, shallow } from 'enzyme'; +import TetherComponent from 'react-tether'; import sinon from 'sinon'; import DropdownMenu from '../DropdownMenu'; @@ -21,13 +22,14 @@ describe('components/dropdown-menu/DropdownMenu', () => { FakeMenu.displayName = 'FakeMenu'; /* eslint-enable */ - const getWrapper = (props = {}) => - shallow( + const getWrapper = (props = {}) => { + return mount( , ); + }; afterEach(() => { sandbox.verifyAndRestore(); @@ -57,7 +59,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should correctly render a single child button with correct props', () => { - const wrapper = shallow( + const wrapper = mount( @@ -76,7 +78,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should set aria-expanded="true" and aria-controls=menuID when menu is open', () => { - const wrapper = shallow( + const wrapper = mount( @@ -84,7 +86,9 @@ describe('components/dropdown-menu/DropdownMenu', () => { ); const instance = wrapper.instance(); - instance.openMenuAndSetFocusIndex(0); + act(() => { + instance.openMenuAndSetFocusIndex(0); + }); wrapper.update(); const button = wrapper.find(FakeButton); @@ -93,7 +97,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should not render child menu when menu is closed', () => { - const wrapper = shallow( + const wrapper = mount( @@ -105,7 +109,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should correctly render a single child menu with correct props when menu is open', () => { - const wrapper = shallow( + const wrapper = mount( @@ -113,7 +117,9 @@ describe('components/dropdown-menu/DropdownMenu', () => { ); const instance = wrapper.instance(); - instance.openMenuAndSetFocusIndex(1); + act(() => { + instance.openMenuAndSetFocusIndex(1); + }); wrapper.update(); const menu = wrapper.find(FakeMenu); @@ -126,125 +132,95 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should render TetherComponent with correct props with correct default values', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); - expect(wrapper.is('TetherComponent')).toBe(true); - expect(wrapper.prop('attachment')).toEqual('top left'); - expect(wrapper.prop('bodyElement')).toEqual(document.body); - expect(wrapper.prop('classPrefix')).toEqual('dropdown-menu'); - expect(wrapper.prop('targetAttachment')).toEqual('bottom left'); - expect(wrapper.prop('constraints')).toEqual([]); - expect(wrapper.prop('enabled')).toBe(false); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.length).toBe(1); + expect(tetherComponent.prop('attachment')).toEqual('top left'); + expect(tetherComponent.prop('renderElementTo')).toEqual(document.body); + expect(tetherComponent.prop('classPrefix')).toEqual('dropdown-menu'); + expect(tetherComponent.prop('targetAttachment')).toEqual('bottom left'); + expect(tetherComponent.prop('constraints')).toEqual([]); + expect(tetherComponent.prop('enabled')).toBe(false); }); test('should render TetherComponent in the body if invalid body element is specified', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ bodyElement: 'foo' }); - expect(wrapper.is('TetherComponent')).toBe(true); - expect(wrapper.prop('attachment')).toEqual('top left'); - expect(wrapper.prop('bodyElement')).toEqual(document.body); - expect(wrapper.prop('classPrefix')).toEqual('dropdown-menu'); - expect(wrapper.prop('targetAttachment')).toEqual('bottom left'); - expect(wrapper.prop('constraints')).toEqual([]); - expect(wrapper.prop('enabled')).toBe(false); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.length).toBe(1); + expect(tetherComponent.prop('attachment')).toEqual('top left'); + expect(tetherComponent.prop('renderElementTo')).toEqual(document.body); + expect(tetherComponent.prop('classPrefix')).toEqual('dropdown-menu'); + expect(tetherComponent.prop('targetAttachment')).toEqual('bottom left'); + expect(tetherComponent.prop('constraints')).toEqual([]); + expect(tetherComponent.prop('enabled')).toBe(false); }); test('should render className in the className is specified', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ className: 'foo' }); - expect(wrapper.is('TetherComponent')).toBe(true); - expect(wrapper.prop('className')).toEqual('foo'); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.length).toBe(1); + expect(tetherComponent.prop('className')).toEqual('foo'); }); test('should render TetherComponent with a specific body element', () => { const bodyEl = document.createElement('div'); - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ bodyElement: bodyEl }); - expect(wrapper.is('TetherComponent')).toBe(true); - expect(wrapper.prop('attachment')).toEqual('top left'); - expect(wrapper.prop('bodyElement')).toEqual(bodyEl); - expect(wrapper.prop('classPrefix')).toEqual('dropdown-menu'); - expect(wrapper.prop('targetAttachment')).toEqual('bottom left'); - expect(wrapper.prop('constraints')).toEqual([]); - expect(wrapper.prop('enabled')).toBe(false); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.length).toBe(1); + expect(tetherComponent.prop('attachment')).toEqual('top left'); + expect(tetherComponent.prop('renderElementTo')).toEqual(bodyEl); + expect(tetherComponent.prop('classPrefix')).toEqual('dropdown-menu'); + expect(tetherComponent.prop('targetAttachment')).toEqual('bottom left'); + expect(tetherComponent.prop('constraints')).toEqual([]); + expect(tetherComponent.prop('enabled')).toBe(false); }); test('should render TetherComponent with correct props when right aligned', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ isRightAligned: true }); - expect(wrapper.prop('attachment')).toEqual('top right'); - expect(wrapper.prop('targetAttachment')).toEqual('bottom right'); - expect(wrapper.prop('enabled')).toBe(false); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('attachment')).toEqual('top right'); + expect(tetherComponent.prop('targetAttachment')).toEqual('bottom right'); + expect(tetherComponent.prop('enabled')).toBe(false); }); test('should render TetherComponent with attachment and targetAttachment props passed in as tetherAttachment and tetherTargetAttachment', () => { const tetherAttachment = 'middle left'; const tetherTargetAttachment = 'middle right'; - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ + isRightAligned: true, + tetherAttachment, + tetherTargetAttachment, + }); - expect(wrapper.prop('attachment')).toEqual(tetherAttachment); - expect(wrapper.prop('targetAttachment')).toEqual(tetherTargetAttachment); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('attachment')).toEqual(tetherAttachment); + expect(tetherComponent.prop('targetAttachment')).toEqual(tetherTargetAttachment); }); test('should render TetherComponent with enabled prop when menu is open', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); - instance.openMenuAndSetFocusIndex(0); + act(() => { + instance.openMenuAndSetFocusIndex(0); + }); wrapper.update(); - expect(wrapper.prop('enabled')).toBe(true); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('enabled')).toBe(true); }); test('should render TetherComponent with scrollParent constraint when constrainToScrollParent=true', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ constrainToScrollParent: true }); - expect(wrapper.prop('constraints')).toEqual([ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('constraints')).toEqual([ { to: 'scrollParent', attachment: 'together', @@ -253,14 +229,10 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should render TetherComponent with window constraint when constrainToScrollParent=true', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ constrainToWindow: true }); - expect(wrapper.prop('constraints')).toEqual([ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('constraints')).toEqual([ { to: 'window', attachment: 'together', @@ -269,14 +241,10 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should render TetherComponent with scrollParent and window constraints when constrainToScrollParent=true and constrainToWindow=true', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ constrainToScrollParent: true, constrainToWindow: true }); - expect(wrapper.prop('constraints')).toEqual([ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('constraints')).toEqual([ { to: 'scrollParent', attachment: 'together', @@ -289,14 +257,10 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should render TetherComponent with window constraints and pinned when constrainToWindowWithPin=true', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ constrainToWindowWithPin: true }); - expect(wrapper.prop('constraints')).toEqual([ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('constraints')).toEqual([ { to: 'window', attachment: 'together', @@ -306,14 +270,10 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should render TetherComponent with window constraints, pinned and scroll parent when constrainToWindowWithPin=true and constrainToScrollParent=true', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ constrainToScrollParent: true, constrainToWindowWithPin: true }); - expect(wrapper.prop('constraints')).toEqual([ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('constraints')).toEqual([ { to: 'scrollParent', attachment: 'together', @@ -329,46 +289,30 @@ describe('components/dropdown-menu/DropdownMenu', () => { describe('openMenuAndSetFocusIndex()', () => { test('should call setState() with correct values', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); - sandbox - .mock(instance) - .expects('setState') - .withArgs({ - isOpen: true, - initialFocusIndex: 1, - }); + sandbox.mock(instance).expects('setState').withArgs({ + isOpen: true, + initialFocusIndex: 1, + }); instance.openMenuAndSetFocusIndex(1); }); }); describe('closeMenu()', () => { test('should call setState() with correct values', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); - sandbox - .mock(instance) - .expects('setState') - .withArgs({ - isOpen: false, - }); + sandbox.mock(instance).expects('setState').withArgs({ + isOpen: false, + }); instance.closeMenu(); }); }); describe('handleButtonClick()', () => { test('should call openMenuAndSetFocusIndex(null) when menu is currently closed', () => { - const wrapper = shallow( + const wrapper = mount( @@ -376,10 +320,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { ); const instance = wrapper.instance(); - sandbox - .mock(instance) - .expects('openMenuAndSetFocusIndex') - .withArgs(null); + sandbox.mock(instance).expects('openMenuAndSetFocusIndex').withArgs(null); wrapper.find(FakeButton).simulate('click', { preventDefault: sandbox.mock(), @@ -393,7 +334,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { stopPropagation: jest.fn(), }; const onMenuClose = jest.fn(); - const wrapper = shallow( + const wrapper = mount( @@ -401,13 +342,15 @@ describe('components/dropdown-menu/DropdownMenu', () => { ); const instance = wrapper.instance(); - instance.openMenuAndSetFocusIndex(1); + act(() => { + instance.openMenuAndSetFocusIndex(1); + }); wrapper.find(FakeButton).simulate('click', event); expect(event.stopPropagation).toBeCalled(); expect(event.preventDefault).toBeCalled(); - expect(onMenuClose).toBeCalledWith(event); + expect(onMenuClose).toBeCalled(); }); }); @@ -424,7 +367,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { }, ].forEach(({ key }) => { test('should call openMenuAndSetFocus(0) when an open keystroke is pressed', () => { - const wrapper = shallow( + const wrapper = mount( @@ -432,10 +375,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { ); const instance = wrapper.instance(); - sandbox - .mock(instance) - .expects('openMenuAndSetFocusIndex') - .withArgs(0); + sandbox.mock(instance).expects('openMenuAndSetFocusIndex').withArgs(0); wrapper.find(FakeButton).simulate('keydown', { key, @@ -445,10 +385,17 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); }); - test('shoud not stop esc propagation if dropdown is closed', () => { + test('should not stop esc propagation if dropdown is closed', () => { const onMenuClose = jest.fn(); - const wrapper = getWrapper({ onMenuClose }); - wrapper.setState({ isOpen: false }); + const wrapper = mount( + + + + , + ); + act(() => { + wrapper.setState({ isOpen: false }); + }); wrapper.find(FakeButton).simulate('keydown', { key: KEYS.escape, @@ -461,8 +408,15 @@ describe('components/dropdown-menu/DropdownMenu', () => { test('should stop esc propagation if dropdown is open', () => { const onMenuClose = jest.fn(); - const wrapper = getWrapper({ onMenuClose }); - wrapper.setState({ isOpen: true }); + const wrapper = mount( + + + + , + ); + act(() => { + wrapper.setState({ isOpen: true }); + }); wrapper.find(FakeButton).simulate('keydown', { key: KEYS.escape, @@ -474,7 +428,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { }); test('should call openMenuAndSetFocus(-1) to last item when "up" is pressed', () => { - const wrapper = shallow( + const wrapper = mount( @@ -482,10 +436,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { ); const instance = wrapper.instance(); - sandbox - .mock(instance) - .expects('openMenuAndSetFocusIndex') - .withArgs(-1); + sandbox.mock(instance).expects('openMenuAndSetFocusIndex').withArgs(-1); wrapper.find(FakeButton).simulate('keydown', { key: 'ArrowUp', @@ -497,12 +448,7 @@ describe('components/dropdown-menu/DropdownMenu', () => { describe('handleMenuClose()', () => { test('should call closeMenu() and focusButton() when called', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); sandbox.mock(instance).expects('closeMenu'); diff --git a/src/components/flyout/Flyout.js b/src/components/flyout/Flyout.js index 0e56a1351d..b3fe1bba68 100644 --- a/src/components/flyout/Flyout.js +++ b/src/components/flyout/Flyout.js @@ -488,14 +488,23 @@ class Flyout extends React.Component { } return ( - - {React.cloneElement(overlayButton, overlayButtonProps)} - {isVisible && ( - - {React.cloneElement(overlayContent, overlayProps)} - + ( +
+ {React.cloneElement(overlayButton, overlayButtonProps)} +
)} -
+ renderElement={ref => { + return isVisible ? ( +
+ + {React.cloneElement(overlayContent, overlayProps)} + +
+ ) : null; + }} + /> ); } } diff --git a/src/components/flyout/Flyout.scss b/src/components/flyout/Flyout.scss index 1cdac848c1..f57a02a295 100644 --- a/src/components/flyout/Flyout.scss +++ b/src/components/flyout/Flyout.scss @@ -1,6 +1,14 @@ @import '../../styles/variables'; @import './variables'; +.bdl-Flyout-target { + display: inline-block; +} + +.bdl-Flyout-element { + outline: none; +} + .flyout-overlay { @include common-typography; diff --git a/src/components/flyout/__tests__/Flyout.test.js b/src/components/flyout/__tests__/Flyout.test.js index 21de997c5a..ca9660dba7 100644 --- a/src/components/flyout/__tests__/Flyout.test.js +++ b/src/components/flyout/__tests__/Flyout.test.js @@ -1,5 +1,6 @@ import React, { act } from 'react'; import { mount, shallow } from 'enzyme'; +import TetherComponent from 'react-tether'; import sinon from 'sinon'; import Flyout from '../Flyout'; @@ -36,6 +37,15 @@ describe('components/flyout/Flyout', () => { FakeOverlay.displayName = 'FakeOverlay'; /* eslint-enable */ + const getWrapper = (props = {}) => { + return mount( + + + + , + ); + }; + afterEach(() => { sandbox.verifyAndRestore(); }); @@ -64,12 +74,7 @@ describe('components/flyout/Flyout', () => { }); test('should correctly render a single child button with correct props', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); const button = wrapper.find(FakeButton); @@ -83,16 +88,14 @@ describe('components/flyout/Flyout', () => { }); test('should set aria-expanded="true" and aria-controls=overlayID when overlay is visible', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); - wrapper.setState({ - isVisible: true, + act(() => { + wrapper.setState({ + isVisible: true, + }); }); + wrapper.update(); const button = wrapper.find(FakeButton); expect(button.prop('aria-expanded')).toEqual('true'); @@ -112,17 +115,15 @@ describe('components/flyout/Flyout', () => { }); test('should correctly render a single child overlay with correct props when overlay is open', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); - wrapper.setState({ - isVisible: true, + act(() => { + wrapper.setState({ + isVisible: true, + }); }); + wrapper.update(); const overlay = wrapper.find(FakeOverlay); expect(overlay.length).toBe(1); @@ -136,80 +137,60 @@ describe('components/flyout/Flyout', () => { }); test('should render TetherComponent with correct props with correct default values', () => { - const wrapper = shallow( - - - - , - ); - expect(wrapper.is('TetherComponent')).toBe(true); - expect(wrapper.prop('attachment')).toEqual('top left'); - expect(wrapper.prop('targetAttachment')).toEqual('bottom left'); - expect(wrapper.prop('classPrefix')).toEqual('flyout-overlay'); - expect(wrapper.prop('enabled')).toBe(false); + const wrapper = getWrapper(); + + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.length).toBe(1); + expect(tetherComponent.prop('attachment')).toEqual('top left'); + expect(tetherComponent.prop('targetAttachment')).toEqual('bottom left'); + expect(tetherComponent.prop('classPrefix')).toEqual('flyout-overlay'); + expect(tetherComponent.prop('enabled')).toBe(false); }); test('should render TetherComponent with correct enable prop when overlay is visible', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper(); - wrapper.setState({ - isVisible: true, + act(() => { + wrapper.setState({ + isVisible: true, + }); }); + wrapper.update(); - expect(wrapper.prop('enabled')).toBe(true); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('enabled')).toBe(true); }); test('should render TetherComponent with offset when offset is passed in as a prop', () => { const offset = 'wooot'; - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ offset }); - expect(wrapper.prop('offset')).toEqual(offset); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('offset')).toEqual(offset); }); test('should render TetherComponent with passed in className', () => { const className = 'the-class-name'; - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ className }); - expect(wrapper.prop('classes')).toEqual({ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('classes')).toEqual({ element: `flyout-overlay ${className}`, }); }); test('should render TetherComponent without scrollParent constraint when constrainToScrollParent=false', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ constrainToScrollParent: false }); - expect(wrapper.prop('constraints')).toEqual([]); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('constraints')).toEqual([]); }); test('should render TetherComponent with window constraint when constrainToWindow=true', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ constrainToWindow: true }); - expect(wrapper.prop('constraints')).toEqual([ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('constraints')).toEqual([ { to: 'scrollParent', attachment: 'together', @@ -222,14 +203,10 @@ describe('components/flyout/Flyout', () => { }); test('should render TetherComponent with window constraint when constrainToWindowWithPin=true', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ constrainToWindowWithPin: true }); - expect(wrapper.prop('constraints')).toEqual([ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('constraints')).toEqual([ { to: 'scrollParent', attachment: 'together', @@ -277,13 +254,10 @@ describe('components/flyout/Flyout', () => { }, ].forEach(({ position, offset }) => { test('should set tether offset correctly when offset props is not passed in', () => { - const wrapper = shallow( - - - - , - ); - expect(wrapper.prop('offset')).toEqual(offset); + const wrapper = getWrapper({ position }); + + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('offset')).toEqual(offset); }); }); }); @@ -313,12 +287,7 @@ describe('components/flyout/Flyout', () => { ])( 'should handle clicks within overlay properly %s', ({ closeOnClick, hasClickableAncestor, shouldCloseOverlay }) => { - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper({ closeOnClick }); const instance = wrapper.instance(); act(() => { instance.setState({ @@ -350,12 +319,7 @@ describe('components/flyout/Flyout', () => { let wrapper = null; beforeEach(() => { - wrapper = mount( - - - - , - ); + wrapper = getWrapper(); instance = wrapper.instance(); }); @@ -403,12 +367,7 @@ describe('components/flyout/Flyout', () => { describe('handleButtonHover()', () => { test('should call openOverlay() when props.openOnHover is true', () => { const event = {}; - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper({ openOnHover: true }); const instance = wrapper.instance(); setTimeout(() => { @@ -420,12 +379,7 @@ describe('components/flyout/Flyout', () => { test('should not call openOverlay() when props.openOnHover is false', () => { const event = {}; - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper({ openOnHover: false }); const instance = wrapper.instance(); setTimeout(() => { @@ -437,12 +391,7 @@ describe('components/flyout/Flyout', () => { test('should be able to set custom timeouts for the openOnHover', () => { const timeout = 100; - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper({ openOnHover: false, openOnHoverDebounceTimeout: timeout }); const instance = wrapper.instance(); setTimeout(() => { @@ -459,12 +408,7 @@ describe('components/flyout/Flyout', () => { describe('handleButtonHoverLeave()', () => { test('should call closeOverlay', () => { - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper({ openOnHover: false }); const instance = wrapper.instance(); @@ -514,32 +458,26 @@ describe('components/flyout/Flyout', () => { }, ].forEach(({ currentIsVisible, isVisibleAfterOverlayClosed }) => { test('should toggle isVisible state when called', () => { - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); const event = { preventDefault: sandbox.stub(), }; - instance.setState({ - isVisible: currentIsVisible, + act(() => { + instance.setState({ + isVisible: currentIsVisible, + }); + }); + act(() => { + instance.closeOverlay(event); }); - instance.closeOverlay(event); expect(instance.state.isVisible).toEqual(isVisibleAfterOverlayClosed); }); }); test('should call onClose when closeOverlay gets called', () => { const onClose = sandbox.mock(); - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ onClose }); const instance = wrapper.instance(); const event = { preventDefault: sandbox.stub(), @@ -560,12 +498,7 @@ describe('components/flyout/Flyout', () => { }, ].forEach(({ currentIsVisible, isVisibleAfterOverlayOpened }) => { test('should toggle isVisible state when called', () => { - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); const event = { preventDefault: sandbox.stub(), @@ -584,12 +517,7 @@ describe('components/flyout/Flyout', () => { test('should call onOpen when openOverlay gets called', () => { const onOpen = sandbox.mock(); - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ onOpen }); const instance = wrapper.instance(); const event = { preventDefault: sandbox.stub(), @@ -900,12 +828,7 @@ describe('components/flyout/Flyout', () => { }, ].forEach(({ prevIsVisible, currIsVisible, shouldAddEventListener, shouldRemoveEventListener }) => { test('should remove and add event listeners properly', () => { - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper({ isVisibleByDefault: prevIsVisible }); const instance = wrapper.instance(); const documentMock = sandbox.mock(document); @@ -940,12 +863,7 @@ describe('components/flyout/Flyout', () => { }, ].forEach(({ isVisible, shouldRemoveEventListener }) => { test('should remove event listeners only when the overlay is visible', () => { - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); const documentMock = sandbox.mock(document); @@ -969,12 +887,7 @@ describe('components/flyout/Flyout', () => { describe('handleOverlayClose()', () => { test('should call focusButton() and closeOverlay() when called', () => { - const wrapper = mount( - - - - , - ); + const wrapper = getWrapper(); const instance = wrapper.instance(); sandbox.mock(instance).expects('focusButton'); @@ -986,14 +899,10 @@ describe('components/flyout/Flyout', () => { describe('isResponsive', () => { test('should have correct className when isResponsive is true', () => { - const wrapper = shallow( - - - - , - ); + const wrapper = getWrapper({ isResponsive: true }); - expect(wrapper.prop('classes')).toEqual({ + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('classes')).toEqual({ element: `flyout-overlay bdl-Flyout--responsive`, }); }); diff --git a/src/components/footer-indicator/FooterIndicator.tsx b/src/components/footer-indicator/FooterIndicator.tsx index 2e23315cdf..ebeecc5162 100644 --- a/src/components/footer-indicator/FooterIndicator.tsx +++ b/src/components/footer-indicator/FooterIndicator.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Tooltip, { TooltipPosition } from '../tooltip'; +import { Focusable, Tooltip, TooltipProvider } from '@box/blueprint-web'; import IconPuzzlePiece from '../../icons/general/IconPuzzlePiece'; import './FooterIndicator.scss'; @@ -11,14 +11,18 @@ type Props = { const FooterIndicator = ({ indicatorText }: Props) => { return (
- -
- - - - {indicatorText} -
-
+ + + +
+ + + + {indicatorText} +
+
+
+
); }; diff --git a/src/components/footer-indicator/__tests__/__snapshots__/FooterIndicator.test.tsx.snap b/src/components/footer-indicator/__tests__/__snapshots__/FooterIndicator.test.tsx.snap index 7b9898adda..3bd79fa31a 100644 --- a/src/components/footer-indicator/__tests__/__snapshots__/FooterIndicator.test.tsx.snap +++ b/src/components/footer-indicator/__tests__/__snapshots__/FooterIndicator.test.tsx.snap @@ -4,31 +4,32 @@ exports[`feature/footer-indicator/FooterIndicator should render a FooterIndicato
- -
+ - - - - - abcdefghijklmnopqrstuvwxyz - -
-
+ +
+ + + + + abcdefghijklmnopqrstuvwxyz + +
+
+ +
`; diff --git a/src/components/label/InfoIconWithTooltip.tsx b/src/components/label/InfoIconWithTooltip.tsx index 0322f0148f..e74afe8629 100644 --- a/src/components/label/InfoIconWithTooltip.tsx +++ b/src/components/label/InfoIconWithTooltip.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; +import { Focusable, Tooltip, TooltipProvider } from '@box/blueprint-web'; import InfoBadge16 from '../../icon/fill/InfoBadge16'; -import Tooltip, { TooltipPosition } from '../tooltip'; export interface InfoIconWithTooltipProps { /** Custom class for the icon */ @@ -14,11 +14,15 @@ export interface InfoIconWithTooltipProps { const InfoIconWithTooltip = ({ className = '', iconProps, tooltipText }: InfoIconWithTooltipProps) => ( - - - - - + + + + + + + + + ); diff --git a/src/components/label/__tests__/__snapshots__/InfoIconWithTooltip.test.tsx.snap b/src/components/label/__tests__/__snapshots__/InfoIconWithTooltip.test.tsx.snap index 9aece997df..318b06e118 100644 --- a/src/components/label/__tests__/__snapshots__/InfoIconWithTooltip.test.tsx.snap +++ b/src/components/label/__tests__/__snapshots__/InfoIconWithTooltip.test.tsx.snap @@ -5,23 +5,22 @@ exports[`components/label/InfoIconWithTooltip should render correctly 1`] = ` className="test-class tooltip-icon-container" key="infoIcon" > - - + - - - + + + + + + + `; diff --git a/src/components/media/Media.scss b/src/components/media/Media.scss index e1e211f007..f009481523 100644 --- a/src/components/media/Media.scss +++ b/src/components/media/Media.scss @@ -4,6 +4,10 @@ .bdl-Media { display: flex; align-items: flex-start; + + .bdl-DropdownMenu-target { + float: right; + } } // the media content, ie, an avatar diff --git a/src/components/media/__tests__/__snapshots__/Media.test.tsx.snap b/src/components/media/__tests__/__snapshots__/Media.test.tsx.snap index c538b6a795..8828b47184 100644 --- a/src/components/media/__tests__/__snapshots__/Media.test.tsx.snap +++ b/src/components/media/__tests__/__snapshots__/Media.test.tsx.snap @@ -15,18 +15,22 @@ exports[`components/Media compound component 1`] = `
- + +
Yo Yo Ma diff --git a/src/components/menu/MenuItem.scss b/src/components/menu/MenuItem.scss new file mode 100644 index 0000000000..9a242931bc --- /dev/null +++ b/src/components/menu/MenuItem.scss @@ -0,0 +1,3 @@ +.bdl-MenuItem-radarTarget { + display: block; +} diff --git a/src/components/menu/MenuItem.tsx b/src/components/menu/MenuItem.tsx index afb3c289cf..986e9adb86 100644 --- a/src/components/menu/MenuItem.tsx +++ b/src/components/menu/MenuItem.tsx @@ -4,6 +4,8 @@ import omit from 'lodash/omit'; import RadarAnimation from '../radar'; +import './MenuItem.scss'; + export interface MenuItemProps { /** 'aria-checked' - ARIA attribute for checkbox elements */ 'aria-checked'?: boolean; @@ -68,7 +70,7 @@ class MenuItem extends React.Component { let menuItem =
  • {children}
  • ; if (showRadar) { - menuItem = {menuItem}; + menuItem = {menuItem}; } return menuItem; diff --git a/src/components/menu/__tests__/__snapshots__/MenuItem.test.tsx.snap b/src/components/menu/__tests__/__snapshots__/MenuItem.test.tsx.snap index a0fdee1d64..2195d4b04a 100644 --- a/src/components/menu/__tests__/__snapshots__/MenuItem.test.tsx.snap +++ b/src/components/menu/__tests__/__snapshots__/MenuItem.test.tsx.snap @@ -8,6 +8,7 @@ exports[`components/menu/MenuItem render() should render a RadarAnimation if sho constrainToWindow={true} isShown={true} position="middle-right" + targetWrapperClassName="bdl-MenuItem-radarTarget" >
  • { wrapper.setState({ selectedIndex: 0 }); }); - expect( - wrapper - .find('Pill') - .at(0) - .prop('isSelected'), - ).toBe(true); + expect(wrapper.find('Pill').at(0).prop('isSelected')).toBe(true); }); test('should render hidden pill selection helper', () => { @@ -282,7 +277,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { sandbox.mock(wrapper.find('textarea').getDOMNode()).expects('focus'); - wrapper.simulate('click'); + wrapper.find('.bdl-PillSelector').simulate('click'); }); }); @@ -310,7 +305,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { sandbox.mock(instance).expects('resetSelectedIndex'); sandbox.mock(wrapper.find('textarea').getDOMNode()).expects('focus'); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'Backspace', preventDefault: sandbox.mock(), stopPropagation: sandbox.mock(), @@ -323,7 +318,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { const wrapper = mount( {}} onInput={onInputStub} onRemove={onRemoveStub} value="test" />, ); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'Backspace', preventDefault: sandbox.mock().never(), stopPropagation: sandbox.mock().never(), @@ -336,7 +331,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { const wrapper = mount( , ); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'Backspace', preventDefault: sandbox.mock(), stopPropagation: sandbox.mock(), @@ -350,7 +345,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { const wrapper = mount( , ); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'Backspace', preventDefault: sandbox.mock(), stopPropagation: sandbox.mock(), @@ -360,7 +355,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { test('should not call onRemove() when backspace is pressed and there are no pills and no input value', () => { const wrapper = mount(); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'Backspace', preventDefault: sandbox.mock().never(), stopPropagation: sandbox.mock().never(), @@ -376,7 +371,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { wrapper.setState({ selectedIndex: 1 }); }); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'ArrowLeft', preventDefault: sandbox.mock(), stopPropagation: sandbox.mock(), @@ -391,7 +386,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { wrapper.setState({ selectedIndex: 0 }); }); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'ArrowLeft', preventDefault: sandbox.mock(), stopPropagation: sandbox.mock(), @@ -411,7 +406,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { sandbox.mock(wrapper.find('[data-testid="pill-selection-helper"]').getDOMNode()).expects('focus'); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'ArrowLeft', preventDefault: sandbox.mock(), stopPropagation: sandbox.mock(), @@ -424,7 +419,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { const wrapper = mount( {}} onInput={onInputStub} onRemove={onRemoveStub} value="test" />, ); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'ArrowLeft', preventDefault: sandbox.mock().never(), stopPropagation: sandbox.mock().never(), @@ -449,7 +444,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { sandbox.mock(instance).expects('resetSelectedIndex'); sandbox.mock(wrapper.find('textarea').getDOMNode()).expects('focus'); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'ArrowRight', preventDefault: sandbox.mock(), stopPropagation: sandbox.mock(), @@ -468,7 +463,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { wrapper.setState({ selectedIndex: 0 }); }); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'ArrowRight', preventDefault: sandbox.mock(), stopPropagation: sandbox.mock(), @@ -479,7 +474,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { test('should not prevent default when right arrow is pressed and no pill is selected', () => { const wrapper = mount(); - wrapper.simulate('keyDown', { + wrapper.find('.bdl-PillSelector').simulate('keyDown', { key: 'ArrowRight', preventDefault: sandbox.mock().never(), stopPropagation: sandbox.mock().never(), @@ -494,10 +489,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { const wrapper = shallow( , ); - wrapper - .find('Pill') - .at(0) - .prop('onRemove')(); + wrapper.find('Pill').at(0).prop('onRemove')(); expect(onRemoveStub.calledWith(option, 0)).toBe(true); }); @@ -507,10 +499,7 @@ describe('components/pill-selector-dropdown/PillSelector', () => { const wrapper = shallow( , ); - wrapper - .find('Pill') - .at(0) - .prop('onRemove')(); + wrapper.find('Pill').at(0).prop('onRemove')(); expect(onRemoveStub.calledWith(option, 0)).toBe(true); }); }); diff --git a/src/components/radar/RadarAnimation.scss b/src/components/radar/RadarAnimation.scss index 085282ce67..81ac1265ab 100644 --- a/src/components/radar/RadarAnimation.scss +++ b/src/components/radar/RadarAnimation.scss @@ -117,3 +117,7 @@ $radar-animation-offset: 1px; .radar-animation-element { z-index: $overlay-z-index; } + +.bdl-RadarAnimation-target { + display: inline-block; +} diff --git a/src/components/radar/RadarAnimation.stories.tsx b/src/components/radar/RadarAnimation.stories.tsx index ead426d643..00d195296d 100644 --- a/src/components/radar/RadarAnimation.stories.tsx +++ b/src/components/radar/RadarAnimation.stories.tsx @@ -58,9 +58,11 @@ export const topRight = () => ( ); export const withOffset = () => ( - - - +
    + + + +
    ); export default { diff --git a/src/components/radar/RadarAnimation.tsx b/src/components/radar/RadarAnimation.tsx index a66778471b..34257546d0 100644 --- a/src/components/radar/RadarAnimation.tsx +++ b/src/components/radar/RadarAnimation.tsx @@ -1,6 +1,9 @@ import * as React from 'react'; +import classNames from 'classnames'; import uniqueId from 'lodash/uniqueId'; -import TetherComponent from 'react-tether'; +import TetherComponent, { type TetherProps } from 'react-tether'; + +import TetherPosition from '../../common/tether-positions'; import './RadarAnimation.scss'; @@ -18,48 +21,50 @@ export enum RadarAnimationPosition { const positions = { [RadarAnimationPosition.BOTTOM_CENTER]: { - attachment: 'top center', - targetAttachment: 'bottom center', + attachment: TetherPosition.TOP_CENTER, + targetAttachment: TetherPosition.BOTTOM_CENTER, }, [RadarAnimationPosition.BOTTOM_LEFT]: { - attachment: 'top left', - targetAttachment: 'bottom left', + attachment: TetherPosition.TOP_LEFT, + targetAttachment: TetherPosition.BOTTOM_LEFT, }, [RadarAnimationPosition.BOTTOM_RIGHT]: { - attachment: 'top right', - targetAttachment: 'bottom right', + attachment: TetherPosition.TOP_RIGHT, + targetAttachment: TetherPosition.BOTTOM_RIGHT, }, [RadarAnimationPosition.MIDDLE_CENTER]: { - attachment: 'middle center', - targetAttachment: 'middle center', + attachment: TetherPosition.MIDDLE_CENTER, + targetAttachment: TetherPosition.MIDDLE_CENTER, }, [RadarAnimationPosition.MIDDLE_LEFT]: { - attachment: 'middle right', - targetAttachment: 'middle left', + attachment: TetherPosition.MIDDLE_RIGHT, + targetAttachment: TetherPosition.MIDDLE_LEFT, }, [RadarAnimationPosition.MIDDLE_RIGHT]: { - attachment: 'middle left', - targetAttachment: 'middle right', + attachment: TetherPosition.MIDDLE_LEFT, + targetAttachment: TetherPosition.MIDDLE_RIGHT, }, [RadarAnimationPosition.TOP_CENTER]: { - attachment: 'bottom center', - targetAttachment: 'top center', + attachment: TetherPosition.BOTTOM_CENTER, + targetAttachment: TetherPosition.TOP_CENTER, }, [RadarAnimationPosition.TOP_LEFT]: { - attachment: 'bottom left', - targetAttachment: 'top left', + attachment: TetherPosition.BOTTOM_LEFT, + targetAttachment: TetherPosition.TOP_LEFT, }, [RadarAnimationPosition.TOP_RIGHT]: { - attachment: 'bottom right', - targetAttachment: 'top right', + attachment: TetherPosition.BOTTOM_RIGHT, + targetAttachment: TetherPosition.TOP_RIGHT, }, }; export interface RadarAnimationProps { /** A React element to put the radar on */ - children: React.ReactChild; + children: React.ReactElement; /** A CSS class for the radar */ className?: string; + /** Optional class name for the target wrapper element */ + targetWrapperClassName?: string; /** Whether to constrain the radar to the element's scroll parent. Defaults to `false` */ constrainToScrollParent: boolean; /** Whether to constrain the radar to window. Defaults to `true` */ @@ -103,6 +108,7 @@ class RadarAnimation extends React.Component { position, isShown, offset, + targetWrapperClassName, tetherElementClassName, ...rest } = this.props; @@ -127,18 +133,17 @@ class RadarAnimation extends React.Component { 'aria-describedby': this.radarAnimationID, }); - // Typescript defs seem busted for older versions of react-tether - const tetherProps: { - attachment: string; - className?: string; - classPrefix: string; - constraints: {}; - targetAttachment: string; + const tetherProps: Pick< + TetherProps, + 'attachment' | 'targetAttachment' | 'constraints' | 'classPrefix' | 'enabled' + > & { offset?: string; + className?: string; } = { attachment, classPrefix: 'radar-animation', constraints, + enabled: isShown, targetAttachment, }; @@ -151,15 +156,21 @@ class RadarAnimation extends React.Component { } return ( - - {referenceElement} - {isShown && ( -
    + ( +
    + {referenceElement} +
    + )} + renderElement={ref => ( +
    )} - + /> ); } } diff --git a/src/components/radar/__tests__/RadarAnimation.test.tsx b/src/components/radar/__tests__/RadarAnimation.test.tsx index bcd8eabac3..3a87f7e053 100644 --- a/src/components/radar/__tests__/RadarAnimation.test.tsx +++ b/src/components/radar/__tests__/RadarAnimation.test.tsx @@ -1,14 +1,17 @@ import * as React from 'react'; -import { shallow } from 'enzyme'; -import RadarAnimation, { RadarAnimationProps, RadarAnimationPosition } from '../RadarAnimation'; +import { mount, shallow } from 'enzyme'; +import TetherComponent from 'react-tether'; +import RadarAnimation, { RadarAnimationPosition } from '../RadarAnimation'; describe('components/radar/RadarAnimation', () => { - const getWrapper = (props: {}) => - shallow( + const getWrapper = (props: {}) => { + return mount(
    Hello
    , ); + }; + [ { // description: @@ -52,7 +55,11 @@ describe('components/radar/RadarAnimation', () => { }, ].forEach(({ position }) => { test(`should render correctly with ${position} positioning`, () => { - const wrapper = getWrapper({ position } as RadarAnimationProps); + const wrapper = shallow( + +
    Hello
    +
    , + ); expect(wrapper).toMatchSnapshot(); }); }); @@ -70,40 +77,37 @@ describe('components/radar/RadarAnimation', () => { offset, }); - expect(wrapper.prop('offset')).toEqual(offset); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('offset')).toEqual(offset); }); test('should render correctly with tetherElementClassName', () => { expect( - getWrapper({ - tetherElementClassName: 'tether-element-class-name', - }), + shallow( + +
    Hello
    +
    , + ), ).toMatchSnapshot(); }); describe('isShown', () => { test('should be shown when isShown is not provided', () => { - expect( - getWrapper({} as RadarAnimationProps) - .find('.radar') - .exists(), - ).toBe(true); + const wrapper = getWrapper({}); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('enabled')).toBe(true); }); test('should be shown when isShown is true', () => { - expect( - getWrapper({ isShown: true } as RadarAnimationProps) - .find('.radar') - .exists(), - ).toBe(true); + const wrapper = getWrapper({ isShown: true }); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('enabled')).toBe(true); }); test('should not be shown when isShown is false', () => { - expect( - getWrapper({ isShown: false } as RadarAnimationProps) - .find('.radar') - .exists(), - ).toBe(false); + const wrapper = getWrapper({ isShown: false }); + const tetherComponent = wrapper.find(TetherComponent); + expect(tetherComponent.prop('enabled')).toBe(false); }); }); @@ -111,7 +115,7 @@ describe('components/radar/RadarAnimation', () => { test.each([true, false])('should only position the tether when shown', isShown => { const positionTetherMock = jest.fn(); - const wrapper = getWrapper({ isShown } as RadarAnimationProps); + const wrapper = getWrapper({ isShown }); // @ts-ignore: react-tether shenanigans wrapper.instance().tetherRef = { current: { position: positionTetherMock } }; diff --git a/src/components/radar/__tests__/__snapshots__/RadarAnimation.test.tsx.snap b/src/components/radar/__tests__/__snapshots__/RadarAnimation.test.tsx.snap index 9c9e44e7e6..129ecf551b 100644 --- a/src/components/radar/__tests__/__snapshots__/RadarAnimation.test.tsx.snap +++ b/src/components/radar/__tests__/__snapshots__/RadarAnimation.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/radar/RadarAnimation should render correctly with bottom-center positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with bottom-left positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with bottom-right positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with middle-center positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with middle-left positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with middle-right positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with tetherElementClassName 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with top-center positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with top-left positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; exports[`components/radar/RadarAnimation should render correctly with top-right positioning 1`] = ` - -
    - Hello -
    -
    -
    -
    -
    - +/> `; diff --git a/src/components/select/Select.js b/src/components/select/Select.js index 93b90e6415..02bd2953c1 100644 --- a/src/components/select/Select.js +++ b/src/components/select/Select.js @@ -63,12 +63,13 @@ const Select = ({ {infoTooltip && ( - - setInfoTooltipIsOpen(!infoTooltipIsOpen)} - > + + setInfoTooltipIsOpen(!infoTooltipIsOpen)}> diff --git a/src/components/text-area/__tests__/TextArea.test.js b/src/components/text-area/__tests__/TextArea.test.js index a71f7cce2d..8b7a3fcadc 100644 --- a/src/components/text-area/__tests__/TextArea.test.js +++ b/src/components/text-area/__tests__/TextArea.test.js @@ -1,4 +1,6 @@ +import { mount, shallow } from 'enzyme'; import * as React from 'react'; +import TetherComponent from 'react-tether'; import TextArea from '../TextArea'; @@ -71,13 +73,11 @@ describe('components/text-area/TextArea', () => { test('should render Tooltip with tetherElementClassName', () => { const className = 'tether-element-class-name'; - const wrapper = shallow(