Skip to content

Commit

Permalink
#4979 - Support for Strict mode (#5013)
Browse files Browse the repository at this point in the history
- fixed ketcher initialisation in react strict mode
- enabled react strict mode in example
- added keydown/mousedown remove listener functions

---------

Co-authored-by: Mikhail Vialov <[email protected]>
  • Loading branch information
accmeboot and Mikhail Vialov authored Jul 12, 2024
1 parent e94a558 commit cecc56f
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 51 deletions.
2 changes: 1 addition & 1 deletion example/.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SKIP_PREFLIGHT_CHECK=true
REACT_APP_API_PATH=/v2
PUBLIC_URL=./
GENERATE_SOURCEMAP = false
GENERATE_SOURCEMAP = false
6 changes: 3 additions & 3 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'ketcher-react/dist/index.css';

import { useState } from 'react';
import { useState, StrictMode } from 'react';
import { ButtonsConfig, Editor, InfoModal } from 'ketcher-react';
import {
Ketcher,
Expand Down Expand Up @@ -87,7 +87,7 @@ const App = () => {
<PolymerEditor togglerComponent={togglerComponent} />
</>
) : (
<>
<StrictMode>
<Editor
errorHandler={(message: string) => {
setHasError(true);
Expand Down Expand Up @@ -121,7 +121,7 @@ const App = () => {
}}
/>
)}
</>
</StrictMode>
);
};

Expand Down
59 changes: 35 additions & 24 deletions packages/ketcher-react/src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
ketcherInitEventName,
KETCHER_ROOT_NODE_CLASS_NAME,
} from './constants';
import { createRoot } from 'react-dom/client';
import { createRoot, Root } from 'react-dom/client';

const mediaSizes = {
smallWidth: 1040,
Expand All @@ -44,37 +44,48 @@ interface EditorProps extends Omit<Config, 'element' | 'appRoot'> {
}

function Editor(props: EditorProps) {
const initPromiseRef = useRef<ReturnType<typeof init> | null>(null);
const appRootRef = useRef<Root | null>(null);
const cleanupRef = useRef<(() => unknown) | null>(null);

const rootElRef = useRef<HTMLDivElement>(null);
const { onInit } = props;

const { height, width } = useResizeObserver<HTMLDivElement>({
ref: rootElRef,
});

useEffect(() => {
const appRoot = createRoot(rootElRef.current as HTMLDivElement);
init({
const initKetcher = () => {
appRootRef.current = createRoot(rootElRef.current as HTMLDivElement);

initPromiseRef.current = init({
...props,
element: rootElRef.current,
appRoot,
}).then(
({
ketcher,
ketcherId,
}: {
ketcher: Ketcher | undefined;
ketcherId: string;
}) => {
if (typeof onInit === 'function' && ketcher) {
onInit(ketcher);
const ketcherInitEvent = new Event(ketcherInitEventName(ketcherId));
window.dispatchEvent(ketcherInitEvent);
}
},
);
appRoot: appRootRef.current,
});

initPromiseRef.current?.then(({ ketcher, ketcherId, cleanup }) => {
cleanupRef.current = cleanup;

if (typeof props.onInit === 'function' && ketcher) {
props.onInit(ketcher);
const ketcherInitEvent = new Event(ketcherInitEventName(ketcherId));
window.dispatchEvent(ketcherInitEvent);
}
});
};
useEffect(() => {
if (initPromiseRef.current === null) {
initKetcher();
} else {
initPromiseRef.current?.finally(() => {
initKetcher();
});
}

return () => {
// setTimeout is used to disable the warn msg from react "Attempted to synchronously unmount a root while React was already rendering"
setTimeout(() => {
appRoot.unmount();
initPromiseRef.current?.then(() => {
cleanupRef.current?.();
appRootRef.current?.unmount();
});
};
// TODO: provide the list of dependencies after implementing unsubscribe function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,20 @@ class KetcherBuilder {
errorHandler: (message: string) => void,
buttons?: ButtonsConfig,
togglerComponent?: JSX.Element,
): Promise<{ setKetcher: (ketcher: Ketcher) => void; ketcherId: string }> {
): Promise<{
setKetcher: (ketcher: Ketcher) => void;
ketcherId: string;
cleanup: ReturnType<typeof initApp> | null;
}> {
const { structService } = this;
let cleanup: ReturnType<typeof initApp> | null = null;

const { editor, setKetcher, ketcherId } = await new Promise<{
editor: Editor;
setKetcher: (ketcher: Ketcher) => void;
ketcherId: string;
}>((resolve) => {
initApp(
cleanup = initApp(
element,
appRoot,
staticResourcesUrl,
Expand All @@ -94,7 +99,8 @@ class KetcherBuilder {
: // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {};
this.formatterFactory = new FormatterFactory(structService!);
return { setKetcher, ketcherId };

return { setKetcher, ketcherId, cleanup };
}

build() {
Expand Down
4 changes: 2 additions & 2 deletions packages/ketcher-react/src/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function buildKetcherAsync({

await builder.appendApiAsync(structServiceProvider);
builder.appendServiceMode(structServiceProvider.mode);
const { setKetcher, ketcherId } = await builder.appendUiAsync(
const { setKetcher, ketcherId, cleanup } = await builder.appendUiAsync(
element,
appRoot,
staticResourcesUrl,
Expand All @@ -55,7 +55,7 @@ async function buildKetcherAsync({
if (ketcher) {
setKetcher(ketcher);
}
return { ketcher, ketcherId };
return { ketcher, ketcherId, cleanup };
}

export type { Config, ButtonsConfig };
Expand Down
10 changes: 8 additions & 2 deletions packages/ketcher-react/src/script/ui/App/initApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import { Provider } from 'react-redux';
import { uniqueId } from 'lodash';
import { Root } from 'react-dom/client';
import createStore from '../state';
import { initKeydownListener } from '../state/hotkeys';
import { initKeydownListener, removeKeydownListener } from '../state/hotkeys';
import { initResize } from '../state/toolbar';
import { initMouseListener } from '../state/mouse';
import { initMouseListener, removeMouseListeners } from '../state/mouse';

function initApp(
element: HTMLDivElement | null,
Expand All @@ -52,6 +52,7 @@ function initApp(
resolve({ editor, setKetcher, ketcherId });
};
const store = createStore(options, server, setEditor);

store.dispatch(initKeydownListener(element));
store.dispatch(initMouseListener(element));
store.dispatch(initResize());
Expand All @@ -73,6 +74,11 @@ function initApp(
</SettingsContext.Provider>
</Provider>,
);

return () => {
store.dispatch(removeKeydownListener(element));
store.dispatch(removeMouseListeners(element));
};
}

export { initApp };
16 changes: 13 additions & 3 deletions packages/ketcher-react/src/script/ui/state/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,22 @@ import {
import { isArrowKey, moveSelectedItems } from './moveSelectedItems';
import { handleHotkeyOverItem } from './handleHotkeysOverItem';

let keydownListener: ((event: KeyboardEvent) => void) | null = null;

export function initKeydownListener(element) {
return function (dispatch, getState) {
const hotKeys = initHotKeys(actions);
element.addEventListener('keydown', (event) =>
keyHandle(dispatch, getState, hotKeys, event),
);

keydownListener = (event) => keyHandle(dispatch, getState, hotKeys, event);
element.addEventListener('keydown', keydownListener);
};
}

export function removeKeydownListener(element) {
return function () {
if (keydownListener) {
element.removeEventListener('keydown', keydownListener);
}
};
}

Expand Down
41 changes: 28 additions & 13 deletions packages/ketcher-react/src/script/ui/state/mouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,47 @@
import { updateCursorPosition } from './common';
import { throttle } from 'lodash';

type mouseListener = ((event: MouseEvent) => void) | null;

const MOUSE_MOVE_THROTTLE_TIMEOUT = 300;

const handleMouseMove = (dispatch, event: MouseEvent) => {
dispatch(updateCursorPosition(event.clientX, event.clientY));
};

let pointerMoveListener: mouseListener = null;
let mouseDownListener: mouseListener = null;

export function initMouseListener(element) {
return function (dispatch, getState) {
const throttledHandleMouseMove = throttle(
handleMouseMove,
MOUSE_MOVE_THROTTLE_TIMEOUT,
);

element.addEventListener('pointermove', (event: MouseEvent) =>
throttledHandleMouseMove(dispatch, event),
);
element.addEventListener(
'mousedown',
function (event: MouseEvent) {
const areBothLeftAndRightButtonsClicked = event.buttons === 3;
if (areBothLeftAndRightButtonsClicked) {
handleRightClick(getState);
}
},
true,
);
pointerMoveListener = (event: MouseEvent) =>
throttledHandleMouseMove(dispatch, event);
mouseDownListener = (event: MouseEvent) => {
const areBothLeftAndRightButtonsClicked = event.buttons === 3;
if (areBothLeftAndRightButtonsClicked) {
handleRightClick(getState);
}
};

element.addEventListener('pointermove', pointerMoveListener);
element.addEventListener('mousedown', mouseDownListener, true);
};
}

export function removeMouseListeners(element) {
return function () {
if (pointerMoveListener) {
element.removeEventListener('pointermove', pointerMoveListener);
}

if (mouseDownListener) {
element.addEventListener('mousedown', mouseDownListener, true);
}
};
}

Expand Down

0 comments on commit cecc56f

Please sign in to comment.