feat: Add scroll listener to the nearest scrollable container and allow consumer to disable it (#3408)

* fix: Add scroll listener to the nearest scrollable container

* fix

* use loop instead of recursion

* fix

* return document

* calculate nearest scrollable container in settimeout to unblock main thread

* Add prop detectNearestScroll and clear timeout on unmount

* disable scroll listener on excal app

* update prop name to detectScroll

* update docs

* remove settimeout

* tweak docs

Co-authored-by: David Luzar <luzar.david@gmail.com>

* tweak changelog

Co-authored-by: David Luzar <luzar.david@gmail.com>

* lint

Co-authored-by: David Luzar <luzar.david@gmail.com>
This commit is contained in:
Aakansha Doshi 2021-04-09 20:44:54 +05:30 committed by GitHub
parent d91950bd03
commit c19c8ecd27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 47 additions and 8 deletions

View File

@ -168,6 +168,7 @@ import { AppProps, AppState, Gesture, GestureEvent, SceneData } from "../types";
import {
debounce,
distance,
getNearestScrollableContainer,
isInputLike,
isToolIcon,
isWritableElement,
@ -303,6 +304,7 @@ class App extends React.Component<AppProps, AppState> {
private scene: Scene;
private resizeObserver: ResizeObserver | undefined;
private nearestScrollableContainer: HTMLElement | Document | undefined;
constructor(props: AppProps) {
super(props);
const defaultAppState = getDefaultAppState();
@ -841,6 +843,10 @@ class App extends React.Component<AppProps, AppState> {
document.removeEventListener(EVENT.COPY, this.onCopy);
document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
document.removeEventListener(EVENT.CUT, this.onCut);
this.nearestScrollableContainer?.removeEventListener(
EVENT.SCROLL,
this.onScroll,
);
document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
document.removeEventListener(
@ -907,8 +913,15 @@ class App extends React.Component<AppProps, AppState> {
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
document.addEventListener(EVENT.CUT, this.onCut);
document.addEventListener(EVENT.SCROLL, this.onScroll);
if (this.props.detectScroll) {
this.nearestScrollableContainer = getNearestScrollableContainer(
this.excalidrawContainerRef.current!,
);
this.nearestScrollableContainer.addEventListener(
EVENT.SCROLL,
this.onScroll,
);
}
window.addEventListener(EVENT.RESIZE, this.onResize, false);
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
window.addEventListener(EVENT.BLUR, this.onBlur, false);

View File

@ -102,7 +102,6 @@ export const TITLE_TIMEOUT = 10000;
export const TOAST_TIMEOUT = 5000;
export const VERSION_TIMEOUT = 30000;
export const SCROLL_TIMEOUT = 100;
export const ZOOM_STEP = 0.1;
// Report a user inactive after IDLE_THRESHOLD milliseconds

View File

@ -324,6 +324,7 @@ const ExcalidrawWrapper = () => {
renderFooter={renderFooter}
langCode={langCode}
renderCustomStats={renderCustomStats}
detectScroll={false}
/>
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
{errorMessage && (

View File

@ -15,6 +15,7 @@ Please add the latest change on the top under the correct section.
## Excalidraw API
- Recompute offsets on `scroll` of the nearest scrollable container [#3408](https://github.com/excalidraw/excalidraw/pull/3408). This can be disabled by setting [`detectScroll`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#detectScroll) to `false`.
- Add `onPaste` prop to handle custom clipboard behaviours [#3420](https://github.com/excalidraw/excalidraw/pull/3420).
## Excalidraw Library

View File

@ -365,6 +365,7 @@ To view the full example visit :point_down:
| [`name`](#name) | string | | Name of the drawing |
| [`UIOptions`](#UIOptions) | <pre>{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }</pre> | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L129) | To customise UI options. Currently we support customising [`canvas actions`](#canvasActions) |
| [`onPaste`](#onPaste) | <pre>(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L17">ClipboardData</a>, event: ClipboardEvent &#124; null) => boolean</pre> | | Callback to be triggered if passed when the something is pasted in to the scene |
| [`detectScroll`](#detectScroll) | boolean | true | Indicates whether to update the offsets when nearest ancestor is scrolled. |
### Dimensions of Excalidraw
@ -587,6 +588,10 @@ useEffect(() => {
Try out the [Demo](#Demo) to see it in action.
### detectScroll
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
### Extra API's
#### `getSceneVersion`

View File

@ -30,6 +30,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
name,
renderCustomStats,
onPaste,
detectScroll = true,
} = props;
const canvasActions = props.UIOptions?.canvasActions;
@ -80,6 +81,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
renderCustomStats={renderCustomStats}
UIOptions={UIOptions}
onPaste={onPaste}
detectScroll={detectScroll}
/>
</InitializeApp>
);
@ -102,11 +104,6 @@ const areEqual = (
);
};
Excalidraw.defaultProps = {
lanCode: defaultLang.code,
UIOptions: DEFAULT_UI_OPTIONS,
};
const forwardedRefComp = forwardRef<
ExcalidrawAPIRefValue,
PublicExcalidrawProps

View File

@ -195,6 +195,7 @@ export interface ExcalidrawProps {
appState: AppState,
) => JSX.Element;
UIOptions?: UIOptions;
detectScroll?: boolean;
}
export type SceneData = {
@ -228,4 +229,5 @@ export type AppProps = ExcalidrawProps & {
UIOptions: {
canvasActions: Required<CanvasActions>;
};
detectScroll: boolean;
};

View File

@ -406,3 +406,24 @@ export const supportsEmoji = () => {
ctx.fillText("😀", 0, 0);
return ctx.getImageData(offset, offset, 1, 1).data[0] !== 0;
};
export const getNearestScrollableContainer = (
element: HTMLElement,
): HTMLElement | Document => {
let parent = element.parentElement;
while (parent) {
if (parent === document.body) {
return document;
}
const { overflowY } = window.getComputedStyle(parent);
const hasScrollableContent = parent.scrollHeight > parent.clientHeight;
if (
hasScrollableContent &&
(overflowY === "auto" || overflowY === "scroll")
) {
return parent;
}
parent = parent.parentElement;
}
return document;
};