Skip to content
This repository was archived by the owner on Sep 20, 2021. It is now read-only.

Commit 7bcdd67

Browse files
committed
Refactored bound methods, need to add testing to make sure set state doesn't get called
1 parent f524e63 commit 7bcdd67

File tree

2 files changed

+107
-83
lines changed

2 files changed

+107
-83
lines changed

src/popover.test.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,21 +259,34 @@ describe("Popover", () => {
259259
});
260260

261261
describe("Uncontrolled component methods", () => {
262-
it("Opens on request", () => {
262+
let spy: jest.SpyInstance<((isOpen: boolean) => void) | undefined>;
263+
beforeEach(() => {
264+
defaultProps = {
265+
...defaultProps,
266+
isOpen: undefined,
267+
onOpenChange: () => {}
268+
};
269+
spy = jest.spyOn(defaultProps, "onOpenChange");
270+
});
271+
272+
it("Opens by method", () => {
263273
let wrapper = shallow(<Popover {...defaultProps} />);
264274

265275
let popperHover = wrapper.find(PopperHover);
266276
expect(popperHover).toHaveLength(1);
267277
expect(popperHover.hasClass("visible")).toBe(false);
278+
expect(spy).not.toHaveBeenCalled();
268279

269280
(wrapper.instance() as Popover).open();
270281

271282
popperHover = wrapper.find(PopperHover);
272283
expect(popperHover).toHaveLength(1);
273284
expect(popperHover.hasClass("visible")).toBe(true);
285+
expect(spy).toHaveBeenCalledTimes(1);
286+
expect(spy).toHaveBeenLastCalledWith(true);
274287
});
275288

276-
it("Closes on request", () => {
289+
it("Opens by hover, closes on request", () => {
277290
let wrapper = shallow(<Popover {...defaultProps} />);
278291

279292
let popperHover = wrapper.find(PopperHover);
@@ -285,12 +298,17 @@ describe("Popover", () => {
285298
popperHover = wrapper.find(PopperHover);
286299
expect(popperHover).toHaveLength(1);
287300
expect(popperHover.hasClass("visible")).toBe(true);
301+
expect(spy).toHaveBeenCalledTimes(1);
302+
expect(spy).toHaveBeenLastCalledWith(true);
288303

304+
spy.mockReset();
289305
(wrapper.instance() as Popover).close();
290306

291307
popperHover = wrapper.find(PopperHover);
292308
expect(popperHover).toHaveLength(1);
293309
expect(popperHover.hasClass("visible")).toBe(false);
310+
expect(spy).toHaveBeenCalledTimes(1);
311+
expect(spy).toHaveBeenLastCalledWith(false);
294312
});
295313
});
296314

@@ -322,7 +340,7 @@ describe("Popover", () => {
322340
popperHover = wrapper.find(PopperHover);
323341
expect(popperHover).toHaveLength(1);
324342
expect(popperHover.hasClass("visible")).toBe(true);
325-
expect(spy).toHaveBeenLastCalledWith(true);
343+
expect(spy).not.toHaveBeenCalled();
326344
});
327345

328346
it("Closes by prop", () => {
@@ -341,7 +359,7 @@ describe("Popover", () => {
341359
popperHover = wrapper.find(PopperHover);
342360
expect(popperHover).toHaveLength(1);
343361
expect(popperHover.hasClass("visible")).toBe(false);
344-
expect(spy).toHaveBeenLastCalledWith(false);
362+
expect(spy).not.toHaveBeenCalled();
345363
});
346364
});
347365
});

src/popover.tsx

Lines changed: 85 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type TargetType = "click" | "hover";
1616
export type PopperType = "blur" | "click" | "hover" | "none";
1717

1818
export interface PopoverProps {
19+
/// Undefined if uncontrolled. If controlled, true for open, false for closed
1920
isOpen?: boolean;
2021
popperClassName?: string;
2122
popperContent?: React.ReactNode;
@@ -26,6 +27,8 @@ export interface PopoverProps {
2627
targetType: TargetType;
2728
wrapperElementProps?: any;
2829
wrapperElementType?: string;
30+
/// If uncontrolled - a callback handler to notify when to change the state
31+
/// If controlled - a callback handler to notify when isOpen should be changed
2932
onOpenChange?: (isOpen: boolean) => void;
3033
}
3134

@@ -55,23 +58,19 @@ export class Popover extends React.Component<PopoverProps, PopoverState> {
5558
public open() {
5659
if (this.props.isOpen != null) {
5760
console.warn(
58-
"Popover's open() can only be used when popover control's it's open state, not when `open` is passed as a prop"
61+
"Popover's open() can only be used when popover control's it's open state, not when `isOpen` is passed as a prop"
5962
);
6063
}
61-
this.setState({
62-
isOpen: true
63-
});
64+
this.onChangeOpen(true);
6465
}
6566

6667
public close() {
6768
if (this.props.isOpen != null) {
6869
console.warn(
69-
"Popover's open() can only be used when popover control's it's open state, not when `open` is passed as a prop"
70+
"Popover's open() can only be used when popover control's it's open state, not when `isOpen` is passed as a prop"
7071
);
7172
}
72-
this.setState({
73-
isOpen: false
74-
});
73+
this.onChangeOpen(false);
7574
}
7675

7776
componentDidUpdate(prevProps: PopoverProps, prevState: PopoverState) {
@@ -82,10 +81,7 @@ export class Popover extends React.Component<PopoverProps, PopoverState> {
8281

8382
const { onOpenChange } = this.props;
8483
if (onOpenChange != null) {
85-
if (this.props.isOpen !== prevProps.isOpen) {
86-
onOpenChange(this.props.isOpen || false);
87-
}
88-
if (this.state.isOpen !== prevState.isOpen) {
84+
if (this.props.isOpen == null && this.state.isOpen !== prevState.isOpen) {
8985
onOpenChange(this.state.isOpen);
9086
}
9187
}
@@ -113,23 +109,14 @@ export class Popover extends React.Component<PopoverProps, PopoverState> {
113109
}
114110

115111
private renderTarget() {
116-
const {
117-
popperType,
118-
targetClassName,
119-
targetContent,
120-
targetType
121-
} = this.props;
112+
const { targetClassName, targetContent, targetType } = this.props;
122113

123114
switch (targetType) {
124115
case "click":
125116
return (
126117
<TargetClick
127118
className={targetClassName}
128-
onClick={() => {
129-
this.setState({
130-
isOpen: !this.state.isOpen
131-
});
132-
}}
119+
onClick={this.onReverseState}
133120
ref={ref => {
134121
this.targetClickRef = ref;
135122
}}
@@ -141,17 +128,7 @@ export class Popover extends React.Component<PopoverProps, PopoverState> {
141128
return (
142129
<TargetHover
143130
className={targetClassName}
144-
onHoverChange={(isHovering: boolean) => {
145-
if (
146-
popperType === "hover" ||
147-
popperType === "none" ||
148-
isHovering
149-
) {
150-
this.setState({
151-
isOpen: isHovering
152-
});
153-
}
154-
}}
131+
onHoverChange={this.onTargetHoverChange}
155132
>
156133
{targetContent}
157134
</TargetHover>
@@ -161,6 +138,14 @@ export class Popover extends React.Component<PopoverProps, PopoverState> {
161138
}
162139
}
163140

141+
private onTargetHoverChange = (isHovering: boolean) => {
142+
const { popperType } = this.props;
143+
144+
if (popperType === "hover" || popperType === "none" || isHovering) {
145+
this.onChangeOpen(isHovering);
146+
}
147+
};
148+
164149
private renderPopper() {
165150
const { popperContent, popperOptions, popperType } = this.props;
166151

@@ -173,86 +158,107 @@ export class Popover extends React.Component<PopoverProps, PopoverState> {
173158
}
174159

175160
switch (popperType) {
176-
case "click":
161+
case "blur":
177162
return (
178-
<PopperClick
163+
<PopperBlur
179164
className={className}
180165
setScheduleUpdate={this.setScheduleUpdate}
181166
{...popperOptions}
182-
onDismiss={() => {
183-
this.setState({
184-
isOpen: false
185-
});
186-
}}
167+
onDismiss={this.onPopperBlurDismiss}
187168
>
188169
{popperContent}
189-
</PopperClick>
170+
</PopperBlur>
190171
);
191-
case "hover":
172+
case "click":
192173
return (
193-
<PopperHover
174+
<PopperClick
194175
className={className}
195176
setScheduleUpdate={this.setScheduleUpdate}
196177
{...popperOptions}
197-
onHoverChange={(isHovering: boolean) => {
198-
this.setState({
199-
isOpen: isHovering
200-
});
201-
}}
178+
onDismiss={this.onClose}
202179
>
203180
{popperContent}
204-
</PopperHover>
181+
</PopperClick>
205182
);
206-
case "none":
183+
case "hover":
207184
return (
208185
<PopperHover
209186
className={className}
210187
setScheduleUpdate={this.setScheduleUpdate}
211188
{...popperOptions}
212-
onHoverChange={(isHovering: boolean) => {}}
189+
onHoverChange={this.onChangeOpen}
213190
>
214191
{popperContent}
215192
</PopperHover>
216193
);
217-
case "blur":
194+
case "none":
218195
return (
219-
<PopperBlur
196+
<PopperHover
220197
className={className}
221198
setScheduleUpdate={this.setScheduleUpdate}
222199
{...popperOptions}
223-
onDismiss={(event: MouseEvent) => {
224-
if (!this.state.isOpen) {
225-
return;
226-
}
227-
if (this.targetClickRef != null) {
228-
let target = ReactDOM.findDOMNode(this.targetClickRef);
229-
let isTargetOrChild = false;
230-
let sourceElement: Element | null = event.srcElement;
231-
while (sourceElement != null) {
232-
if (target == sourceElement) {
233-
isTargetOrChild = true;
234-
break;
235-
}
236-
sourceElement = sourceElement.parentElement;
237-
}
238-
239-
if (isTargetOrChild) {
240-
return;
241-
}
242-
}
243-
this.setState({
244-
isOpen: false
245-
});
246-
}}
200+
onHoverChange={() => {}}
247201
>
248202
{popperContent}
249-
</PopperBlur>
203+
</PopperHover>
250204
);
251205
default:
252206
throw new Error("Target type must be either click or hover");
253207
}
254208
}
255209

210+
private onPopperBlurDismiss = (event: MouseEvent) => {
211+
if (
212+
(this.state.isOpen === false && this.props.isOpen == null) ||
213+
this.props.isOpen === false
214+
) {
215+
return;
216+
}
217+
if (this.targetClickRef != null) {
218+
let target = ReactDOM.findDOMNode(this.targetClickRef);
219+
let isTargetOrChild = false;
220+
let sourceElement: null | Element = event.srcElement;
221+
while (sourceElement != null) {
222+
if (target == sourceElement) {
223+
isTargetOrChild = true;
224+
break;
225+
}
226+
sourceElement = sourceElement.parentElement;
227+
}
228+
229+
if (isTargetOrChild) {
230+
return;
231+
}
232+
}
233+
234+
this.onClose();
235+
};
236+
237+
private onClose = () => {
238+
this.onChangeOpen(false);
239+
};
240+
241+
private onReverseState = () => {
242+
if (this.props.isOpen == null) {
243+
this.onChangeOpen(!this.state.isOpen);
244+
} else {
245+
this.onChangeOpen(!this.props.isOpen);
246+
}
247+
};
248+
249+
private onChangeOpen = (newIsOpen: boolean) => {
250+
const { isOpen, onOpenChange } = this.props;
251+
if (isOpen != null && onOpenChange != null) {
252+
if (isOpen != newIsOpen) {
253+
onOpenChange(newIsOpen);
254+
}
255+
} else if (this.state.isOpen != newIsOpen) {
256+
this.setState({
257+
isOpen: newIsOpen
258+
});
259+
}
260+
};
261+
256262
private setScheduleUpdate = (ref: null | (() => void)) => {
257263
if (ref != null) {
258264
this.scheduleUpdate = ref;

0 commit comments

Comments
 (0)