Skip to content

Commit d24eefc

Browse files
authored
fix(image): Update reply popup when file scale/rotation changes (#529)
1 parent 98a6486 commit d24eefc

File tree

9 files changed

+125
-21
lines changed

9 files changed

+125
-21
lines changed

src/common/BaseAnnotator.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import EventEmitter from './EventEmitter';
55
import i18n from '../utils/i18n';
66
import messages from '../messages';
77
import { Event, IntlOptions, LegacyEvent, Permissions } from '../@types';
8-
import { setIsInitialized } from '../store';
98
import './BaseAnnotator.scss';
109

1110
export type Container = string | HTMLElement;
@@ -109,15 +108,13 @@ export default class BaseAnnotator extends EventEmitter {
109108
this.annotatedEl.classList.add(CSS_LOADED_CLASS);
110109
this.containerEl.classList.add(CSS_CONTAINER_CLASS);
111110

112-
// Update the store with the options provided by preview
113-
this.store.dispatch(store.setRotationAction(rotation));
114-
this.store.dispatch(store.setScaleAction(scale));
115-
116111
// Defer to the child class to render annotations
117112
this.render();
118113

119114
// Update the store now that annotations have been rendered
120-
this.store.dispatch(setIsInitialized());
115+
this.store.dispatch(store.setRotationAction(rotation));
116+
this.store.dispatch(store.setScaleAction(scale));
117+
this.store.dispatch(store.setIsInitialized());
121118
}
122119

123120
public removeAnnotation = (annotationId: string): void => {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react';
2+
import { mount, ReactWrapper } from 'enzyme';
3+
import usePrevious from '../usePrevious';
4+
5+
describe('src/common/usePrevious', () => {
6+
function TestComponent({ value }: { value: string }): JSX.Element {
7+
const previousValue = usePrevious(value);
8+
9+
return (
10+
<div>
11+
<div data-testid="current">{value}</div>
12+
<div data-testid="previous">{previousValue}</div>
13+
</div>
14+
);
15+
}
16+
17+
const getWrapper = (value: string): ReactWrapper => mount(<TestComponent value={value} />);
18+
19+
test('should render the current and previous value, if one is present', () => {
20+
const wrapper = getWrapper('test');
21+
22+
expect(wrapper.find('[data-testid="current"]').text()).toEqual('test');
23+
expect(wrapper.find('[data-testid="previous"]').text()).toEqual('');
24+
25+
wrapper.setProps({ value: 'new!' });
26+
27+
expect(wrapper.find('[data-testid="current"]').text()).toEqual('new!');
28+
expect(wrapper.find('[data-testid="previous"]').text()).toEqual('test');
29+
});
30+
});

src/common/usePrevious.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
3+
// Returns the last provided value, then caches the new value via an effect
4+
export default function usePrevious<T>(value: T): T | undefined {
5+
const ref = React.useRef<T>();
6+
7+
// The useEffect callback is only invoked after the parent component has rendered
8+
React.useEffect(() => {
9+
ref.current = value;
10+
}, [value]);
11+
12+
return ref.current;
13+
}

src/components/Popups/PopupReply.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import React from 'react';
1+
import * as React from 'react';
2+
import * as Popper from '@popperjs/core';
3+
import * as ReactRedux from 'react-redux';
24
import PopupBase from './PopupBase';
35
import ReplyForm from '../ReplyForm';
4-
6+
import usePrevious from '../../common/usePrevious';
7+
import { getScale, getRotation } from '../../store/options';
58
import './PopupReply.scss';
69

710
export type Props = {
@@ -14,13 +17,13 @@ export type Props = {
1417
value?: string;
1518
};
1619

17-
export const options = {
20+
export const options: Partial<Popper.Options> = {
1821
modifiers: [
1922
{
2023
name: 'arrow',
2124
options: {
2225
element: '.ba-Popup-arrow',
23-
padding: 20,
26+
padding: 10,
2427
},
2528
},
2629
{
@@ -53,8 +56,22 @@ export default function PopupReply({
5356
value = '',
5457
...rest
5558
}: Props): JSX.Element {
59+
const popupRef = React.useRef<PopupBase>(null);
60+
const rotation = ReactRedux.useSelector(getRotation);
61+
const prevRotation = usePrevious(rotation);
62+
const scale = ReactRedux.useSelector(getScale);
63+
const prevScale = usePrevious(scale);
64+
65+
React.useEffect(() => {
66+
const { current: popup } = popupRef;
67+
68+
if (popup?.popper && (rotation !== prevRotation || scale !== prevScale)) {
69+
popup.popper.update();
70+
}
71+
}, [popupRef, rotation, scale]); // eslint-disable-line react-hooks/exhaustive-deps
72+
5673
return (
57-
<PopupBase options={options} {...rest}>
74+
<PopupBase ref={popupRef} options={options} {...rest}>
5875
<ReplyForm
5976
isPending={isPending}
6077
onCancel={onCancel}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from 'react';
2+
3+
export default class PopupBase extends React.Component<{ children: React.ReactNode }> {
4+
name = 'PopupBaseMock';
5+
6+
render(): JSX.Element {
7+
const { children } = this.props;
8+
9+
return <div>{children}</div>;
10+
}
11+
}

src/components/Popups/__tests__/PopupReply-test.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,55 @@
1-
import React from 'react';
2-
import { shallow, ShallowWrapper } from 'enzyme';
1+
import * as React from 'react';
2+
import * as ReactRedux from 'react-redux';
3+
import { mount, ReactWrapper } from 'enzyme';
4+
import PopupBase from '../PopupBase';
35
import PopupReply, { Props } from '../PopupReply';
46
import ReplyForm from '../../ReplyForm';
57

6-
jest.mock('../PopupBase', () => ({
7-
__esModule: true,
8-
default: ({ children }: { children: React.ReactNode }): JSX.Element => <div>{children}</div>,
8+
jest.mock('react-redux', () => ({
9+
useSelector: jest.fn(),
910
}));
1011

11-
type PropsIndex = {
12-
[key: string]: Function | HTMLElement | boolean;
13-
};
12+
jest.mock('../PopupBase');
13+
jest.mock('../../ReplyForm');
1414

1515
describe('components/Popups/PopupReply', () => {
16-
const defaults: Props & PropsIndex = {
16+
const defaults: Props = {
1717
isPending: false,
1818
onCancel: jest.fn(),
1919
onChange: jest.fn(),
2020
onSubmit: jest.fn(),
2121
reference: document.createElement('div'),
2222
};
2323

24-
const getWrapper = (props = {}): ShallowWrapper => shallow(<PopupReply {...defaults} {...props} />);
24+
const getWrapper = (props = {}): ReactWrapper => mount(<PopupReply {...defaults} {...props} />);
25+
26+
beforeEach(() => {
27+
jest.spyOn(React, 'useEffect').mockImplementation(f => f());
28+
});
29+
30+
describe('state changes', () => {
31+
test('should call update on the underlying popper instance when the store changes', () => {
32+
const popupMock = { popper: { update: jest.fn() } };
33+
const reduxSpy = jest.spyOn(ReactRedux, 'useSelector').mockImplementation(() => false);
34+
const refSpy = jest.spyOn(React, 'useRef').mockImplementation(() => ({ current: popupMock }));
35+
const wrapper = getWrapper();
36+
37+
reduxSpy.mockReturnValueOnce(true);
38+
wrapper.setProps({ value: '1' });
39+
40+
reduxSpy.mockReturnValueOnce(false);
41+
wrapper.setProps({ value: '2' });
42+
43+
expect(refSpy).toHaveBeenCalled();
44+
expect(popupMock.popper.update).toHaveBeenCalledTimes(3);
45+
});
46+
});
2547

2648
describe('render()', () => {
2749
test('should render the ReplyForm', () => {
2850
const wrapper = getWrapper();
2951

52+
expect(wrapper.exists(PopupBase)).toBe(true);
3053
expect(wrapper.exists(ReplyForm)).toBe(true);
3154
});
3255
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from 'react';
2+
3+
export default class ReplyForm extends React.Component<{ children: React.ReactNode }> {
4+
name = 'ReplyFormMock';
5+
6+
render(): JSX.Element {
7+
const { children } = this.props;
8+
9+
return <div>{children}</div>;
10+
}
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './ReplyForm';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './ReplyForm';

0 commit comments

Comments
 (0)