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 | | [`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 | | [`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>` | | [`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 ### Storing custom data on Excalidraw elements

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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