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:
parent
d91950bd03
commit
c19c8ecd27
@ -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);
|
||||
|
@ -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
|
||||
|
@ -324,6 +324,7 @@ const ExcalidrawWrapper = () => {
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
renderCustomStats={renderCustomStats}
|
||||
detectScroll={false}
|
||||
/>
|
||||
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
||||
{errorMessage && (
|
||||
|
@ -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
|
||||
|
@ -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 | 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`
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
21
src/utils.ts
21
src/utils.ts
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user