Skip to content

Commit cfdcbd3

Browse files
authored
[v5] [table] fix: stop using findDOMNode in Draggable interactions (#6137)
1 parent 6d02b26 commit cfdcbd3

18 files changed

+164
-90
lines changed

packages/core/src/components/editable-text/editableText.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ export interface EditableTextProps extends IntentProps, Props {
5555
*/
5656
disabled?: boolean;
5757

58+
/**
59+
* Ref to attach to the root element rendered by this component.
60+
*
61+
* N.B. this may be renamed to simply `ref` in a future major version of Blueprint, when this class component is
62+
* refactored into a function.
63+
*/
64+
elementRef?: React.RefObject<HTMLDivElement>;
65+
5866
/** Whether the component is currently being edited. */
5967
isEditing?: boolean;
6068

@@ -205,7 +213,7 @@ export class EditableText extends AbstractPureComponent<EditableTextProps, Edita
205213
}
206214

207215
public render() {
208-
const { alwaysRenderInput, disabled, multiline, contentId } = this.props;
216+
const { alwaysRenderInput, disabled, elementRef, multiline, contentId } = this.props;
209217
const value = this.props.value ?? this.state.value;
210218
const hasValue = value != null && value !== "";
211219

@@ -247,7 +255,7 @@ export class EditableText extends AbstractPureComponent<EditableTextProps, Edita
247255
const spanProps: React.HTMLProps<HTMLSpanElement> = contentId != null ? { id: contentId } : {};
248256

249257
return (
250-
<div className={classes} onFocus={this.handleFocus} tabIndex={tabIndex}>
258+
<div className={classes} onFocus={this.handleFocus} tabIndex={tabIndex} ref={elementRef}>
251259
{alwaysRenderInput || this.state.isEditing ? this.renderInput(value) : undefined}
252260
{shouldHideContents ? undefined : (
253261
<span

packages/core/src/components/popover/popover.tsx

+18-22
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,7 @@ import classNames from "classnames";
1919
import * as React from "react";
2020
import { Manager, Modifier, Popper, PopperChildrenProps, Reference, ReferenceChildrenProps } from "react-popper";
2121

22-
import {
23-
AbstractPureComponent,
24-
Classes,
25-
DISPLAYNAME_PREFIX,
26-
HTMLDivProps,
27-
mergeRefs,
28-
refHandler,
29-
Utils,
30-
} from "../../common";
22+
import { AbstractPureComponent, Classes, DISPLAYNAME_PREFIX, HTMLDivProps, refHandler, Utils } from "../../common";
3123
import * as Errors from "../../common/errors";
3224
import { Overlay } from "../overlay/overlay";
3325
import { ResizeSensor } from "../resize-sensor/resizeSensor";
@@ -174,17 +166,20 @@ export class Popover<
174166
* DOM element that contains the popover.
175167
* When `usePortal={true}`, this element will be portaled outside the usual DOM flow,
176168
* so this reference can be very useful for testing.
169+
*
170+
* @public for testing
177171
*/
178172
public popoverElement: HTMLElement | null = null;
179173

180-
/** DOM element that contains the target. */
181-
public targetElement: HTMLElement | null = null;
182-
183174
/** Popover ref handler */
184175
private popoverRef: React.Ref<HTMLDivElement> = refHandler(this, "popoverElement", this.props.popoverRef);
185176

186-
/** Target ref handler */
187-
private targetRef: React.Ref<HTMLElement> = el => (this.targetElement = el);
177+
/**
178+
* Target DOM element ref.
179+
*
180+
* @public for testing
181+
*/
182+
public targetRef = React.createRef<HTMLElement>();
188183

189184
private cancelOpenTimeout?: () => void;
190185

@@ -243,7 +238,7 @@ export class Popover<
243238

244239
return (
245240
<Manager>
246-
<Reference>{this.renderTarget}</Reference>
241+
<Reference innerRef={this.targetRef}>{this.renderTarget}</Reference>
247242
<Popper
248243
innerRef={this.popoverRef}
249244
placement={placement ?? positionToPlacement(position)}
@@ -319,7 +314,7 @@ export class Popover<
319314
*/
320315
public reposition = () => this.popperScheduleUpdate?.();
321316

322-
private renderTarget = ({ ref: popperChildRef }: ReferenceChildrenProps) => {
317+
private renderTarget = ({ ref }: ReferenceChildrenProps) => {
323318
const { children, className, fill, openOnTargetFocus, renderTarget } = this.props;
324319
const { isOpen } = this.state;
325320
const isControlled = this.isControlled();
@@ -330,8 +325,6 @@ export class Popover<
330325
targetTagName = "div";
331326
}
332327

333-
const ref = mergeRefs(popperChildRef, this.targetRef);
334-
335328
const targetEventHandlers: PopoverHoverTargetHandlers<T> | PopoverClickTargetHandlers<T> =
336329
isHoverInteractionKind
337330
? {
@@ -412,8 +405,10 @@ export class Popover<
412405
target = wrappedTarget;
413406
}
414407

408+
// N.B. we must attach the ref ('wrapped' with react-popper functionality) to the DOM element here and
409+
// let ResizeSensor know about it
415410
return (
416-
<ResizeSensor targetRef={ref} onResize={this.reposition}>
411+
<ResizeSensor targetRef={this.targetRef} onResize={this.reposition}>
417412
{target}
418413
</ResizeSensor>
419414
);
@@ -670,14 +665,14 @@ export class Popover<
670665
};
671666

672667
private handleOverlayClose = (e?: React.SyntheticEvent<HTMLElement>) => {
673-
if (this.targetElement === null || e === undefined) {
668+
if (this.targetRef.current == null || e === undefined) {
674669
return;
675670
}
676671

677672
const event = (e.nativeEvent ?? e) as Event;
678673
const eventTarget = (event.composed ? event.composedPath()[0] : event.target) as HTMLElement;
679674
// if click was in target, target event listener will handle things, so don't close
680-
if (!Utils.elementIsOrContains(this.targetElement, eventTarget) || e.nativeEvent instanceof KeyboardEvent) {
675+
if (!Utils.elementIsOrContains(this.targetRef.current, eventTarget) || e.nativeEvent instanceof KeyboardEvent) {
681676
this.setOpenState(false, e);
682677
}
683678
};
@@ -716,7 +711,8 @@ export class Popover<
716711

717712
private updateDarkParent() {
718713
if (this.props.usePortal && this.state.isOpen) {
719-
const hasDarkParent = this.targetElement != null && this.targetElement.closest(`.${Classes.DARK}`) != null;
714+
const hasDarkParent =
715+
this.targetRef.current != null && this.targetRef.current.closest(`.${Classes.DARK}`) != null;
720716
this.setState({ hasDarkParent });
721717
}
722718
}

packages/core/src/components/resize-sensor/resize-sensor.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@# Resize sensor
22

3-
ResizeSensor observes the DOM and provides a callback for `"resize"` events on a single child element.
3+
__ResizeSensor__ observes the DOM and provides a callback for `"resize"` events on a single child element.
44
It is a thin wrapper around [`ResizeObserver`][resizeobserver] to provide React bindings.
55

66
[resizeobserver]: https://developers.google.com/web/updates/2016/10/resizeobserver

packages/core/src/components/resize-sensor/resizeSensor.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export interface ResizeSensorProps {
5555
* If you attach a `ref` to the child yourself when rendering it, you must pass the
5656
* same value here (otherwise, ResizeSensor won't be able to attach its own).
5757
*/
58-
targetRef?: React.Ref<any>;
58+
targetRef?: React.RefObject<HTMLElement>;
5959
}
6060

6161
/**
@@ -68,7 +68,7 @@ export interface ResizeSensorProps {
6868
export class ResizeSensor extends AbstractPureComponent<ResizeSensorProps> {
6969
public static displayName = `${DISPLAYNAME_PREFIX}.ResizeSensor`;
7070

71-
private targetRef = React.createRef<HTMLElement>();
71+
private targetRef = this.props.targetRef ?? React.createRef<HTMLElement>();
7272

7373
private prevElement: HTMLElement | undefined = undefined;
7474

packages/core/test/popover/popoverTests.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ describe("<Popover>", () => {
827827

828828
const instance = wrapper.instance() as Popover<React.HTMLProps<HTMLButtonElement>>;
829829
wrapper.popoverElement = instance.popoverElement!;
830-
wrapper.targetElement = instance.targetElement!;
830+
wrapper.targetElement = instance.targetRef.current!;
831831
wrapper.assertFindClass = (className: string, expected = true, msg = className) => {
832832
const actual = wrapper!.findClass(className);
833833
if (expected) {

packages/table/src/cell/editableCell.tsx

+12-10
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export interface EditableCellProps extends CellProps {
7070
/**
7171
* Props that should be passed to the EditableText when it is used to edit
7272
*/
73-
editableTextProps?: EditableTextProps;
73+
editableTextProps?: Omit<EditableTextProps, "elementRef">;
7474
}
7575

7676
export interface EditableCellState {
@@ -90,13 +90,9 @@ export class EditableCell extends React.Component<EditableCellProps, EditableCel
9090
wrapText: false,
9191
};
9292

93-
private cellRef: HTMLElement | null | undefined;
93+
private cellRef = React.createRef<HTMLDivElement>();
9494

95-
private refHandlers = {
96-
cell: (ref: HTMLElement | null) => {
97-
this.cellRef = ref;
98-
},
99-
};
95+
private contentsRef = React.createRef<HTMLDivElement>();
10096

10197
public constructor(props: EditableCellProps) {
10298
super(props);
@@ -146,6 +142,7 @@ export class EditableCell extends React.Component<EditableCellProps, EditableCel
146142
{...editableTextProps}
147143
isEditing={true}
148144
className={classNames(Classes.TABLE_EDITABLE_TEXT, Classes.TABLE_EDITABLE_NAME, className)}
145+
elementRef={this.contentsRef}
149146
intent={spreadableProps.intent}
150147
minWidth={0}
151148
onCancel={this.handleCancel}
@@ -163,7 +160,11 @@ export class EditableCell extends React.Component<EditableCellProps, EditableCel
163160
[Classes.TABLE_NO_WRAP_TEXT]: !wrapText,
164161
});
165162

166-
cellContents = <div className={textClasses}>{savedValue}</div>;
163+
cellContents = (
164+
<div className={textClasses} ref={this.contentsRef}>
165+
{savedValue}
166+
</div>
167+
);
167168
}
168169

169170
return (
@@ -172,14 +173,15 @@ export class EditableCell extends React.Component<EditableCellProps, EditableCel
172173
wrapText={wrapText}
173174
truncated={false}
174175
interactive={interactive}
175-
cellRef={this.refHandlers.cell}
176+
cellRef={this.cellRef}
176177
onKeyPress={this.handleKeyPress}
177178
>
178179
<Draggable
179180
onActivate={this.handleCellActivate}
180181
onDoubleClick={this.handleCellDoubleClick}
181182
preventDefault={false}
182183
stopPropagation={interactive}
184+
targetRef={this.contentsRef}
183185
>
184186
{cellContents}
185187
</Draggable>
@@ -206,7 +208,7 @@ export class EditableCell extends React.Component<EditableCellProps, EditableCel
206208
private checkShouldFocus() {
207209
if (this.props.isFocused && !this.state.isEditing) {
208210
// don't focus if we're editing -- we'll lose the fact that we're editing
209-
this.cellRef?.focus();
211+
this.cellRef.current?.focus();
210212
}
211213
}
212214

packages/table/src/cell/editableCell2.tsx

+12-10
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export interface EditableCell2Props extends Omit<CellProps, "onKeyDown" | "onKey
7070
/**
7171
* Props that should be passed to the EditableText when it is used to edit
7272
*/
73-
editableTextProps?: EditableTextProps;
73+
editableTextProps?: Omit<EditableTextProps, "elementRef">;
7474
}
7575

7676
export interface EditableCell2State {
@@ -92,13 +92,9 @@ export class EditableCell2 extends React.Component<EditableCell2Props, EditableC
9292
wrapText: false,
9393
};
9494

95-
private cellRef: HTMLElement | null | undefined;
95+
private cellRef = React.createRef<HTMLDivElement>();
9696

97-
private refHandlers = {
98-
cell: (ref: HTMLElement | null) => {
99-
this.cellRef = ref;
100-
},
101-
};
97+
private contentsRef = React.createRef<HTMLDivElement>();
10298

10399
public state: EditableCell2State = {
104100
isEditing: false,
@@ -157,6 +153,7 @@ export class EditableCell2 extends React.Component<EditableCell2Props, EditableC
157153
{...editableTextProps}
158154
isEditing={true}
159155
className={classNames(Classes.TABLE_EDITABLE_TEXT, Classes.TABLE_EDITABLE_NAME, className)}
156+
elementRef={this.contentsRef}
160157
intent={spreadableProps.intent}
161158
minWidth={0}
162159
onCancel={this.handleCancel}
@@ -174,7 +171,11 @@ export class EditableCell2 extends React.Component<EditableCell2Props, EditableC
174171
[Classes.TABLE_NO_WRAP_TEXT]: !wrapText,
175172
});
176173

177-
cellContents = <div className={textClasses}>{savedValue}</div>;
174+
cellContents = (
175+
<div className={textClasses} ref={this.contentsRef}>
176+
{savedValue}
177+
</div>
178+
);
178179
}
179180

180181
return (
@@ -183,7 +184,7 @@ export class EditableCell2 extends React.Component<EditableCell2Props, EditableC
183184
wrapText={wrapText}
184185
truncated={false}
185186
interactive={interactive}
186-
cellRef={this.refHandlers.cell}
187+
cellRef={this.cellRef}
187188
onKeyDown={handleKeyDown}
188189
onKeyPress={this.handleKeyPress}
189190
onKeyUp={handleKeyUp}
@@ -194,6 +195,7 @@ export class EditableCell2 extends React.Component<EditableCell2Props, EditableC
194195
onDoubleClick={this.handleCellDoubleClick}
195196
preventDefault={false}
196197
stopPropagation={interactive}
198+
targetRef={this.contentsRef}
197199
>
198200
{cellContents}
199201
</Draggable>
@@ -204,7 +206,7 @@ export class EditableCell2 extends React.Component<EditableCell2Props, EditableC
204206
private checkShouldFocus() {
205207
if (this.props.isFocused && !this.state.isEditing) {
206208
// don't focus if we're editing -- we'll lose the fact that we're editing
207-
this.cellRef?.focus();
209+
this.cellRef.current?.focus();
208210
}
209211
}
210212

packages/table/src/common/contextMenuTargetWrapper.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface ContextMenuTargetWrapperProps extends Props {
2929
children?: React.ReactNode;
3030
renderContextMenu: (e: React.MouseEvent<HTMLElement>) => JSX.Element | undefined;
3131
style: React.CSSProperties;
32+
targetRef?: React.RefObject<HTMLDivElement>;
3233
}
3334

3435
/**
@@ -40,9 +41,9 @@ export interface ContextMenuTargetWrapperProps extends Props {
4041
@ContextMenuTargetLegacy
4142
export class ContextMenuTargetWrapper extends React.PureComponent<ContextMenuTargetWrapperProps> {
4243
public render() {
43-
const { className, children, style } = this.props;
44+
const { className, children, targetRef, style } = this.props;
4445
return (
45-
<div className={className} style={style}>
46+
<div className={className} style={style} ref={targetRef}>
4647
{children}
4748
</div>
4849
);

packages/table/src/deprecatedAliases.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export {
1818
/** @deprecated import ColumnHeaderCell instead */
1919
ColumnHeaderCell as ColumnHeaderCell2,
2020
/** @deprecated import ColumnHeaderCellProps instead */
21-
ColumnHeaderCellProps as ColumnHeaderCellProps2,
21+
type ColumnHeaderCellProps as ColumnHeaderCellProps2,
2222
} from "./headers/columnHeaderCell";
2323

2424
export {
2525
/** @deprecated import RowHeaderCell instead */
2626
RowHeaderCell as RowHeaderCell2,
2727
/** @deprecated import RowHeaderCellProps instead */
28-
RowHeaderCellProps as RowHeaderCellProps2,
28+
type RowHeaderCellProps as RowHeaderCellProps2,
2929
} from "./headers/rowHeaderCell";

0 commit comments

Comments
 (0)