Skip to content

Commit 02bc171

Browse files
authored
feat(react-tether): BREAKING CHANGE Bump BUIE to React 19 (#4278)
* feat(react-tether): Upgrade react-tether to 3.0.3 BREAKING CHANGE: React 17 is no longer supported. This package now requires React 18 or 19. react-tether has been upgraded to v3, so components using react-tether may affect some layouts. * fix: update .flowconfig to take .cjs * fix: Replace Checkbox, InfoIcon, and FooterIndicator tooltips with BP * fix: Use createRoot instead of ReactDOM render * fix: Respond to feedback * fix: Round 2 * fix: flow tests for SidebarNavButton * fix: legacy comment styling
1 parent 953b6f6 commit 02bc171

File tree

85 files changed

+2634
-2322
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+2634
-2322
lines changed

.flowconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ sharedmemory.hash_table_pow=21
2929
esproposal.export_star_as=enable
3030
esproposal.optional_chaining=enable
3131
module.file_ext=.js
32+
module.file_ext=.cjs
3233
module.file_ext=.scss
3334
module.name_mapper.extension='scss' -> '<PROJECT_ROOT>/flow/EmptyFlowStub.js.flow'
3435
module.name_mapper.extension='css' -> '<PROJECT_ROOT>/flow/EmptyFlowStub.js.flow'

package.json

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@
266266
"react-responsive": "^10.0.0",
267267
"react-router-dom": "^5.3.4",
268268
"react-scrollbars-custom": "^4.0.21",
269-
"react-tether": "^1.0.5",
269+
"react-tether": "^3.0.3",
270270
"react-textarea-autosize": "^8.5.3",
271271
"regenerator-runtime": "^0.14.1",
272272
"remarkable": "^2.0.1",
@@ -332,11 +332,11 @@
332332
"mousetrap": "^1.6.3",
333333
"pikaday": "^1.8.0",
334334
"query-string": "5.1.1",
335-
"react": "^17.0.1 || ^18.0.0",
335+
"react": "^18.0.0 || ^19.0.0",
336336
"react-animate-height": "^3.2.3",
337337
"react-aria-components": "^1.10.1",
338338
"react-beautiful-dnd": "^13.1.1",
339-
"react-dom": "^17.0.1 || ^18.0.0",
339+
"react-dom": "^18.0.0 || ^19.0.0",
340340
"react-draggable": "^4.5.0",
341341
"react-immutable-proptypes": "^2.1.0",
342342
"react-intl": ">=2.9.0",
@@ -347,7 +347,7 @@
347347
"react-responsive": "^10.0.0",
348348
"react-router-dom": "5.3.4",
349349
"react-scrollbars-custom": "^4.0.21",
350-
"react-tether": "^1.0.5",
350+
"react-tether": "^3.0.3",
351351
"react-textarea-autosize": "^8.5.3",
352352
"regenerator-runtime": "^0.13.2",
353353
"remarkable": "^2.0.1",
@@ -357,11 +357,6 @@
357357
"tabbable": "^1.1.2",
358358
"uuid": "^8.3.2"
359359
},
360-
"comments": {
361-
"dependencies": {
362-
"react-tether": "Version 2.x has too many breaking changes and requires forwardRef on all components"
363-
}
364-
},
365360
"msw": {
366361
"workerDirectory": [".storybook/public"]
367362
}

src/components/button-group/ButtonGroup.scss

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343

4444
&,
4545
& > .bdl-targeted-click-through {
46-
> .btn {
46+
> .btn,
47+
> .bdl-Tooltip-target > .btn {
4748
margin: 5px 0 5px -1px;
4849
border-radius: 0;
4950

@@ -64,25 +65,29 @@
6465
}
6566
}
6667

67-
> .btn:first-child {
68+
> .btn:first-child,
69+
> .bdl-Tooltip-target:first-child > .btn {
6870
border-top-left-radius: $bdl-border-radius-size-med;
6971
border-bottom-left-radius: $bdl-border-radius-size-med;
7072
}
7173

72-
> .btn:last-child {
74+
> .btn:last-child,
75+
> .bdl-Tooltip-target:last-child > .btn {
7376
border-top-right-radius: $bdl-border-radius-size-med;
7477
border-bottom-right-radius: $bdl-border-radius-size-med;
7578
}
7679

77-
> .btn.is-selected {
80+
> .btn.is-selected,
81+
> .bdl-Tooltip-target > .btn.is-selected {
7882
z-index: 2; /* place on top of siblings */
7983
color: $bdl-gray-80;
8084
background-color: $bdl-gray-10;
8185
border-color: $bdl-gray-65;
8286
box-shadow: none;
8387
}
8488

85-
> .btn:focus {
89+
> .btn:focus,
90+
> .bdl-Tooltip-target > .btn:focus {
8691
z-index: 3; /* place on top of all other buttons for accessibility */
8792
}
8893
}
@@ -98,7 +103,7 @@
98103
border: 1px solid $bdl-gray-30;
99104
box-shadow: none;
100105
cursor: default;
101-
opacity: .4;
106+
opacity: 0.4;
102107
}
103108

104109
> .btn-primary {

src/components/checkbox/Checkbox.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
.checkbox-tooltip-wrapper {
7676
display: inline-flex;
7777
vertical-align: text-bottom;
78+
line-height: 0.1; // This keeps the tooltip wrapper height consistent with the child element
7879

7980
> .info-tooltip {
8081
position: relative;

src/components/checkbox/CheckboxTooltip.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as React from 'react';
22
import { defineMessages, FormattedMessage } from 'react-intl';
33

4+
import { Focusable, Tooltip, TooltipProvider } from '@box/blueprint-web';
5+
46
import IconInfo from '../../icons/general/IconInfo';
5-
import Tooltip from '../tooltip';
67

78
const messages = defineMessages({
89
checkboxTooltipIconInfoText: {
@@ -18,15 +19,19 @@ export interface CheckboxTooltipProps {
1819

1920
const CheckboxTooltip = ({ tooltip }: CheckboxTooltipProps) => (
2021
<div className="checkbox-tooltip-wrapper">
21-
<Tooltip text={tooltip}>
22-
<div className="info-tooltip">
23-
<IconInfo
24-
height={16}
25-
title={<FormattedMessage {...messages.checkboxTooltipIconInfoText} />}
26-
width={16}
27-
/>
28-
</div>
29-
</Tooltip>
22+
<TooltipProvider>
23+
<Tooltip content={tooltip}>
24+
<Focusable>
25+
<div className="info-tooltip">
26+
<IconInfo
27+
height={16}
28+
title={<FormattedMessage {...messages.checkboxTooltipIconInfoText} />}
29+
width={16}
30+
/>
31+
</div>
32+
</Focusable>
33+
</Tooltip>
34+
</TooltipProvider>
3035
</div>
3136
);
3237

src/components/context-menu/ContextMenu.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import TetherComponent from 'react-tether';
2+
import TetherComponent, { TetherProps } from 'react-tether';
33
import uniqueId from 'lodash/uniqueId';
44

55
import './ContextMenu.scss';
@@ -158,20 +158,29 @@ class ContextMenu extends React.Component<ContextMenuProps, ContextMenuState> {
158158
onClose: this.handleMenuClose,
159159
};
160160

161-
// TypeScript defs don't work for older versions of react-tether
162-
const tetherProps = {
161+
const tetherProps: TetherProps = {
163162
attachment: 'top left',
164163
classPrefix: 'context-menu',
165164
constraints,
166165
targetAttachment: 'top left',
167166
targetOffset,
167+
renderElementTo: document.body,
168168
};
169169

170170
return (
171-
<TetherComponent {...tetherProps}>
172-
{React.isValidElement(menuTarget) ? React.cloneElement(menuTarget, menuTargetProps) : null}
173-
{isOpen && React.isValidElement(menu) ? React.cloneElement(menu, menuProps) : null}
174-
</TetherComponent>
171+
<TetherComponent
172+
{...tetherProps}
173+
renderTarget={ref => {
174+
return React.isValidElement(menuTarget) ? (
175+
<div ref={ref}>{React.cloneElement(menuTarget, menuTargetProps)}</div>
176+
) : null;
177+
}}
178+
renderElement={ref => {
179+
return isOpen && React.isValidElement(menu) ? (
180+
<div ref={ref}>{React.cloneElement(menu, menuProps)}</div>
181+
) : null;
182+
}}
183+
/>
175184
);
176185
}
177186
}

src/components/context-menu/__tests__/ContextMenu.test.tsx

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// @ts-ignore
33
import React, { act } from 'react';
44
import { mount, shallow, ReactWrapper } from 'enzyme';
5+
import TetherComponent from 'react-tether';
56
import sinon from 'sinon';
67

78
import ContextMenu, { ContextMenuProps, ContextMenuState } from '../ContextMenu';
@@ -53,7 +54,7 @@ describe('components/context-menu/ContextMenu', () => {
5354
});
5455

5556
test('should correctly render a single child button with correct props', () => {
56-
const wrapper = shallow<ContextMenu>(
57+
const wrapper = mount<ContextMenu>(
5758
<ContextMenu>
5859
<FakeButton />
5960
<FakeMenu />
@@ -69,7 +70,7 @@ describe('components/context-menu/ContextMenu', () => {
6970
});
7071

7172
test('should not render child menu when menu is closed', () => {
72-
const wrapper = shallow(
73+
const wrapper = mount(
7374
<ContextMenu>
7475
<FakeButton />
7576
<FakeMenu />
@@ -81,13 +82,16 @@ describe('components/context-menu/ContextMenu', () => {
8182
});
8283

8384
test('should correctly render a single child menu with correct props when menu is open', () => {
84-
const wrapper = shallow<ContextMenu>(
85+
const wrapper = mount<ContextMenu>(
8586
<ContextMenu>
8687
<FakeButton />
8788
<FakeMenu />
8889
</ContextMenu>,
8990
);
90-
wrapper.setState({ isOpen: true });
91+
act(() => {
92+
wrapper.setState({ isOpen: true });
93+
});
94+
wrapper.update();
9195

9296
const instance = wrapper.instance();
9397

@@ -100,17 +104,18 @@ describe('components/context-menu/ContextMenu', () => {
100104
});
101105

102106
test('should render TetherComponent with correct props with correct default values', () => {
103-
const wrapper = shallow(
107+
const wrapper = mount(
104108
<ContextMenu>
105109
<FakeButton />
106110
<FakeMenu />
107111
</ContextMenu>,
108112
);
109113

110-
expect(wrapper.is('TetherComponent')).toBe(true);
111-
expect(wrapper.prop('attachment')).toEqual('top left');
112-
expect(wrapper.prop('targetAttachment')).toEqual('top left');
113-
expect(wrapper.prop('constraints')).toEqual([]);
114+
const tetherComponent = wrapper.find(TetherComponent);
115+
expect(tetherComponent.length).toBe(1);
116+
expect(tetherComponent.prop('attachment')).toEqual('top left');
117+
expect(tetherComponent.prop('targetAttachment')).toEqual('top left');
118+
expect(tetherComponent.prop('constraints')).toEqual([]);
114119
});
115120

116121
test('should render TetherComponent with constraints when specified', () => {
@@ -153,12 +158,9 @@ describe('components/context-menu/ContextMenu', () => {
153158
</ContextMenu>,
154159
);
155160
const instance = wrapper.instance();
156-
sandbox
157-
.mock(instance)
158-
.expects('setState')
159-
.withArgs({
160-
isOpen: false,
161-
});
161+
sandbox.mock(instance).expects('setState').withArgs({
162+
isOpen: false,
163+
});
162164
instance.closeMenu();
163165
});
164166

@@ -247,7 +249,9 @@ describe('components/context-menu/ContextMenu', () => {
247249
const instance = wrapper.instance();
248250
document.addEventListener = jest.fn();
249251
document.removeEventListener = jest.fn();
250-
instance.setState({ isOpen: true });
252+
act(() => {
253+
instance.setState({ isOpen: true });
254+
});
251255
expect(document.addEventListener).not.toHaveBeenCalledWith('click', expect.anything(), expect.anything());
252256
expect(document.addEventListener).not.toHaveBeenCalledWith(
253257
'contextmenu',
@@ -290,14 +294,8 @@ describe('components/context-menu/ContextMenu', () => {
290294
);
291295

292296
const documentMock = sandbox.mock(document);
293-
documentMock
294-
.expects('removeEventListener')
295-
.withArgs('contextmenu')
296-
.never();
297-
documentMock
298-
.expects('removeEventListener')
299-
.withArgs('click')
300-
.never();
297+
documentMock.expects('removeEventListener').withArgs('contextmenu').never();
298+
documentMock.expects('removeEventListener').withArgs('click').never();
301299

302300
wrapper.unmount();
303301
});
@@ -411,18 +409,18 @@ describe('components/context-menu/ContextMenu', () => {
411409
const instance = wrapper.instance() as ContextMenu;
412410
instance.closeMenu = closeMenuSpy;
413411

414-
const handleContextMenuEvent = ({
412+
const handleContextMenuEvent = {
415413
clientX: 10,
416414
clientY: 15,
417415
preventDefault: preventDefaultSpy,
418-
} as unknown) as MouseEvent;
416+
} as unknown as MouseEvent;
419417
act(() => {
420418
instance.handleContextMenu(handleContextMenuEvent);
421419
});
422420

423-
const documentClickEvent = ({
421+
const documentClickEvent = {
424422
target: document.createElement('div'),
425-
} as unknown) as MouseEvent;
423+
} as unknown as MouseEvent;
426424
instance.handleDocumentClick(documentClickEvent);
427425
expect(closeMenuSpy).toHaveBeenCalled();
428426
});
@@ -438,18 +436,18 @@ describe('components/context-menu/ContextMenu', () => {
438436
const instance = wrapper.instance() as ContextMenu;
439437
instance.closeMenu = closeMenuSpy;
440438

441-
const handleContextMenuEvent = ({
439+
const handleContextMenuEvent = {
442440
clientX: 10,
443441
clientY: 15,
444442
preventDefault: preventDefaultSpy,
445-
} as unknown) as MouseEvent;
443+
} as unknown as MouseEvent;
446444
act(() => {
447445
instance.handleContextMenu(handleContextMenuEvent);
448446
});
449447

450-
const documentClickEvent = ({
448+
const documentClickEvent = {
451449
target: document.getElementById(instance.menuID),
452-
} as unknown) as MouseEvent;
450+
} as unknown as MouseEvent;
453451
instance.handleDocumentClick(documentClickEvent);
454452
expect(closeMenuSpy).not.toHaveBeenCalled();
455453
});

0 commit comments

Comments
 (0)