feat: add props.renderScrollbars (#9399)

* Expose renderScrollbars to AppState

* fix: scrollbar rendering should use al renderable elements

* remove `appState.renderScrollbars`

* clean unused

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Jack Walsh 2025-04-20 07:50:44 +10:00 committed by GitHub
parent b5d60973b7
commit 5fc13e4309
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 53 additions and 16 deletions

View file

@ -31,6 +31,7 @@ All `props` are _optional_.
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean &#124; undefined)</code> | \_ | use for custom src url validation |
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
### Storing custom data on Excalidraw elements

View file

@ -104,6 +104,7 @@ export default function ExampleApp({
const [viewModeEnabled, setViewModeEnabled] = useState(false);
const [zenModeEnabled, setZenModeEnabled] = useState(false);
const [gridModeEnabled, setGridModeEnabled] = useState(false);
const [renderScrollbars, setRenderScrollbars] = useState(false);
const [blobUrl, setBlobUrl] = useState<string>("");
const [canvasUrl, setCanvasUrl] = useState<string>("");
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
@ -192,6 +193,7 @@ export default function ExampleApp({
}) => setPointerData(payload),
viewModeEnabled,
zenModeEnabled,
renderScrollbars,
gridModeEnabled,
theme,
name: "Custom name of drawing",
@ -710,6 +712,14 @@ export default function ExampleApp({
/>
Grid mode
</label>
<label>
<input
type="checkbox"
checked={renderScrollbars}
onChange={() => setRenderScrollbars(!renderScrollbars)}
/>
Render scrollbars
</label>
<label>
<input
type="checkbox"

View file

@ -1218,3 +1218,18 @@ export const elementCenterPoint = (
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
};
/** hack for Array.isArray type guard not working with readonly value[] */
export const isReadonlyArray = (value?: any): value is readonly any[] => {
return Array.isArray(value);
};
export const sizeOf = (
value: readonly number[] | Readonly<Map<any, any>> | Record<any, any>,
): number => {
return isReadonlyArray(value)
? value.length
: value instanceof Map
? value.size
: Object.keys(value).length;
};

View file

@ -1,6 +1,11 @@
import rough from "roughjs/bin/rough";
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
import {
rescalePoints,
arrayToMap,
invariant,
sizeOf,
} from "@excalidraw/common";
import {
degreesToRadians,
@ -57,6 +62,7 @@ import type {
ElementsMap,
ExcalidrawRectanguloidElement,
ExcalidrawEllipseElement,
ElementsMapOrArray,
} from "./types";
import type { Drawable, Op } from "roughjs/bin/core";
import type { Point as RoughPoint } from "roughjs/bin/geometry";
@ -938,10 +944,10 @@ export const getElementBounds = (
};
export const getCommonBounds = (
elements: readonly ExcalidrawElement[],
elements: ElementsMapOrArray,
elementsMap?: ElementsMap,
): Bounds => {
if (!elements.length) {
if (!sizeOf(elements)) {
return [0, 0, 0, 0];
}

View file

@ -1830,6 +1830,9 @@ class App extends React.Component<AppProps, AppState> {
}
scale={window.devicePixelRatio}
appState={this.state}
renderScrollbars={
this.props.renderScrollbars === true
}
device={this.device}
renderInteractiveSceneCallback={
this.renderInteractiveSceneCallback

View file

@ -34,6 +34,7 @@ type InteractiveCanvasProps = {
selectionNonce: number | undefined;
scale: number;
appState: InteractiveCanvasAppState;
renderScrollbars: boolean;
device: Device;
renderInteractiveSceneCallback: (
data: RenderInteractiveSceneCallback,
@ -143,7 +144,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
remotePointerUsernames,
remotePointerUserStates,
selectionColor,
renderScrollbars: false,
renderScrollbars: props.renderScrollbars,
},
device: props.device,
callback: props.renderInteractiveSceneCallback,
@ -230,7 +231,8 @@ const areEqual = (
// on appState)
prevProps.elementsMap !== nextProps.elementsMap ||
prevProps.visibleElements !== nextProps.visibleElements ||
prevProps.selectedElements !== nextProps.selectedElements
prevProps.selectedElements !== nextProps.selectedElements ||
prevProps.renderScrollbars !== nextProps.renderScrollbars
) {
return false;
}

View file

@ -53,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
renderEmbeddable,
aiEnabled,
showDeprecatedFonts,
renderScrollbars,
} = props;
const canvasActions = props.UIOptions?.canvasActions;
@ -143,6 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
renderEmbeddable={renderEmbeddable}
aiEnabled={aiEnabled !== false}
showDeprecatedFonts={showDeprecatedFonts}
renderScrollbars={renderScrollbars}
>
{children}
</App>

View file

@ -1182,7 +1182,7 @@ const _renderInteractiveScene = ({
let scrollBars;
if (renderConfig.renderScrollbars) {
scrollBars = getScrollBars(
visibleElements,
elementsMap,
normalizedWidth,
normalizedHeight,
appState,

View file

@ -6,6 +6,7 @@ import {
toBrandedType,
isDevEnv,
isTestEnv,
isReadonlyArray,
} from "@excalidraw/common";
import { isNonDeletedElement } from "@excalidraw/element";
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
@ -292,11 +293,9 @@ class Scene {
}
replaceAllElements(nextElements: ElementsMapOrArray) {
const _nextElements =
// ts doesn't like `Array.isArray` of `instanceof Map`
nextElements instanceof Array
? nextElements
: Array.from(nextElements.values());
const _nextElements = isReadonlyArray(nextElements)
? nextElements
: Array.from(nextElements.values());
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
validateIndicesThrottled(_nextElements);

View file

@ -2,24 +2,22 @@ import { getGlobalCSSVariable } from "@excalidraw/common";
import { getCommonBounds } from "@excalidraw/element/bounds";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import { getLanguage } from "../i18n";
import type { InteractiveCanvasAppState } from "../types";
import type { ScrollBars } from "./types";
import type { RenderableElementsMap, ScrollBars } from "./types";
export const SCROLLBAR_MARGIN = 4;
export const SCROLLBAR_WIDTH = 6;
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
export const getScrollBars = (
elements: readonly ExcalidrawElement[],
elements: RenderableElementsMap,
viewportWidth: number,
viewportHeight: number,
appState: InteractiveCanvasAppState,
): ScrollBars => {
if (!elements.length) {
if (!elements.size) {
return {
horizontal: null,
vertical: null,

View file

@ -601,6 +601,7 @@ export interface ExcalidrawProps {
) => JSX.Element | null;
aiEnabled?: boolean;
showDeprecatedFonts?: boolean;
renderScrollbars?: boolean;
}
export type SceneData = {