feat(joint-react): add a fitToContent prop and usePaper().fitToContent() - #3501
Open
samuelgja wants to merge 9 commits into
Open
feat(joint-react): add a fitToContent prop and usePaper().fitToContent()#3501samuelgja wants to merge 9 commits into
samuelgja wants to merge 9 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Framing a freshly mounted diagram currently takes a hand-written effect in every
app, and getting it right means solving two problems that have nothing to do with
the app: React-rendered elements have no size until they are measured, so the fit
has to wait for a measurement pass; and when a
<PaperScroller>is mounted it —not the paper — owns the viewport, so
paper.transformToFitContent()fits to thewrong box. It derives its fitting box from
paper.getComputedSize(), which undera scroller is the full sheet rather than the visible window, so the content is
scaled wrongly and the scroller is never scrolled to it.
This adds a
fitToContentprop that solves both, plus a matching one-shot onusePaper().packages/joint-react/src/components/paper/paper.types.tsfitToContent?: boolean | FitToContentOptionsonPaperProps.FitToContentOptionsis a union discriminated bymode, each arm extending thematching core option type —
FitToContentZoomOptions extends Readonly<dia.Paper.TransformToFitContentOptions>,FitToContentResizeOptions extends Readonly<dia.Paper.FitToContentOptions>. Every core option istherefore forwarded and documented once, in core, and
modenarrows without acast.
mode: 'zoom',refit: 'resize',useModelGeometry: true, andverticalAlign/horizontalAlign: 'middle'(core defaults to top/left; the effects thisreplaces all centre).
packages/joint-react/src/utils/fit-to-content.ts(new)normalizeFitOptions()resolvesboolean | FitToContentOptionsinto oneobject with
modeandrefitalways present.runFit()executes the fit against the correct owner. A<PaperScroller>registers itself into this package's own
PaperStore.featuresunder thepaperScrollerkey, so the scroller is found there and the fit routed toscroller.zoomToRect(contentArea, { minScale, maxScale, ...options })— thesame call
usePaperScroller().zoomToFit()makes, so the two never disagree.Feature.instanceis typedunknown, so it is narrowed by a user-defined typeguard against a locally declared
FitScrollerLike; no@joint/plusimport, noas, noanyanywhere in the new code.mode: 'resize'under a scroller is a no-op with a dev warning: the scrollersets the paper's dimensions through
adjustPaperand overwrites it next tick.packages/joint-react/src/hooks/use-fit-to-content.ts(new)'resize'adds aResizeObserveron the fit host (the scroller's element whenone owns the paper, otherwise
paper.el);'always'adds graphadd/remove/reset/change:position/change:sizeand later measurementpasses.
refit, so changing an unrelatedfit option never re-subscribes; option values are read from a latest-ref at fit
time. All triggers funnel through the existing
simpleSchedulermicrotaskbatch, so a burst of resize callbacks produces one fit.
mode: 'resize'changes the paper's own size and so feeds its own observer.The guard records the host size at each fit and drops the callback reporting
that same size. A timer-based guard was tried first and rejected: browsers run
no
requestAnimationFramein a background tab, so a flag cleared on the nextframe latches on forever and blocks every later fit.
fitToContentis ignored, with a dev warning, whentransformis also set —both write the viewport matrix and the outcome would depend on effect order.
packages/joint-react/src/hooks/use-paper.tsPaperApi.fitToContent(options?), the same executor as the prop. Preferredover calling
paper.transformToFitContent()from app code, because it iscorrect under a
<PaperScroller>and the raw paper call is not. The existingJSDoc example is updated to use it.
Tests
src/utils/__tests__/fit-to-content.test.ts— 14 cases: option normalizationand defaults for both arms, the scroller type guard (including partial
look-alikes), zoom/resize dispatch, scroller routing with zoom-bound clamping,
the resize-under-scroller skip, the zero-area guard, and host resolution.
src/hooks/__tests__/use-fit-to-content.test.tsx— 9 cases covering eachrefit policy, the resize-mode echo guard, and the
transformconflict.src/hooks/__tests__/use-fit-to-content.compiler.test.tsx— the trigger wiringagain under
babel-plugin-react-compiler, so auto-memoization cannot silentlystop a fit being scheduled.
src/hooks/__tests__/use-paper.test.tsx— the imperative one-shot.stories/examples/fit-to-content/— an example switching between all fourpresets, with an "Add far element" button that shows
'always're-framingwhere
'once'does not.yarn testpasses on this branch: typecheck, lint, knip, and Jest on React 19(1087) and React 18 (1080).
Beyond the suite, the behaviour was verified in a browser, because green unit
tests do not show whether anything is actually framed. In headless Chrome
against the story: initial fit centres at scale 0.832; shrinking the host from
1366 to 698 px re-fits to 0.542 under
refit: 'resize'and leaves the transformbyte-identical under
'once';mode: 'resize'grows the paper SVG to 1044×446instead of scaling; the imperative call recovers from a manual pan; no console
warnings. The scroller path was then exercised against a real
ui.PaperScroller— a throwaway suite run inside@joint/react-plus, whichsymlinks this package, and deleted afterwards so that package is untouched. It
confirmed
zoomToRectreceives the correct content area and the scroller's ownzoom bounds, and that the only
transformToFitContentcall carries thescroller's explicit
fittingBBoxrather than coming from this code directly.Changesets:
@joint/reactminor — one for the<Paper />prop, one forusePaper.Motivation and Context
Re-lands #3498, which was merged to
devand then reverted so it could gothrough review properly. This branch is the same nine commits cherry-picked
onto the reverted
dev: the eight from #3498 plus the follow-up that addressedits Copilot review threads —
normalizeFitOptionsre-applying every Reactdefault with
??after the spread (an explicitundefinedno longer erasesverticalAlign/horizontalAlign/useModelGeometry), and the fitre-running when any option changes even if
refitis unchanged (acontent-signature dependency, so an equal inline object is still a no-op).
Apps repeat the same effect to frame a diagram on mount — in
@joint/plusappsas
paperScroller.centerContent({ useModelGeometry: true })in auseEffect, and in@joint/reactas auseOnElementsMeasuredcallback callingpaper.transformToFitContent(...), which is what the shipped flowchart demodoes. Requested on the JointJS project board (item 237936138) to make fitting a
prop rather than boilerplate.
This is
@joint/reactonly.@joint/react-plusis unchanged, andusePaperScroller().zoomToFit()stays as it is; the new code produces the sameframing so the two agree.
Notes
devas a next-minor feature. PRs todevget no CI in this repo, sothe full suite was run locally, on this branch, against a freshly built
@joint/coredist — Jest resolves the prebuilt core, and a stale one silentlyfails unrelated suites.
PaperStore.claimFit()ownership seam was added. It would be dead codeuntil
@joint/react-plusopts in, and the structural lookup covers thescroller today. If a second viewport owner ever appears, that is the point to
add one.
useFitToContentis deliberately not exported. The prop covers<Paper>,including the external-paper form, and
usePaper().fitToContent()coversimperative use; exporting the hook later is a one-line change.
mode: 'zoom'withminScale: 1, maxScale: 1iscenterContent.Screenshots (if appropriate):
Not attached — the example story (
Examples/Fit to content) demonstrates eachpreset interactively.