|
89 | 89 |
|
90 | 90 | const resizerId = Symbol(); |
91 | 91 |
|
92 | | - // When a SashLayer ancestor is present the resizer teleports into its |
93 | | - // sash-container so it escapes overflow:hidden on pane wrappers. The |
94 | | - // SashLayer must be scoped to the same scroll context as the viewport |
95 | | - // so that getBoundingClientRect differences remain scroll-invariant. |
96 | | - const layerCtx = getContext<SashLayerContext | undefined>(SASH_LAYER); |
97 | | - let inLayer = $state(false); |
| 92 | + // Resizer requires a SashLayer ancestor and always renders in that overlay. |
| 93 | + // Keep SashLayer in the same scroll context as the viewport so |
| 94 | + // getBoundingClientRect differences remain scroll-invariant. |
| 95 | + const layerCtxMaybe = getContext<SashLayerContext | undefined>(SASH_LAYER); |
| 96 | + if (!layerCtxMaybe) { |
| 97 | + throw new Error("Resizer must be used inside <SashLayer>."); |
| 98 | + } |
| 99 | + const layerCtx = layerCtxMaybe; |
98 | 100 |
|
99 | 101 | let initial = 0; |
100 | 102 | let isResizing = $state(false); |
|
167 | 169 | lastShiftSyncedValue = undefined; |
168 | 170 |
|
169 | 171 | if (pausedAutoLayout) { |
170 | | - layerCtx?.setAutoLayoutPaused(false); |
| 172 | + layerCtx.setAutoLayoutPaused(false); |
171 | 173 | pausedAutoLayout = false; |
172 | 174 | } |
173 | 175 | } |
|
196 | 198 | parseFloat(orientation === "horizontal" ? resizerDiv.style.left : resizerDiv.style.top) || |
197 | 199 | 0; |
198 | 200 | } |
199 | | - if (inLayer && !resizeGroup && !pausedAutoLayout) { |
200 | | - layerCtx?.setAutoLayoutPaused(true); |
| 201 | + if (!resizeGroup && !pausedAutoLayout) { |
| 202 | + layerCtx.setAutoLayoutPaused(true); |
201 | 203 | pausedAutoLayout = true; |
202 | 204 | } |
203 | 205 |
|
|
249 | 251 | const { newValue, overflow } = applyLimits(offsetRem); |
250 | 252 |
|
251 | 253 | if (newValue !== undefined && !passive && !disabled) { |
252 | | - // Fast path when the handle lives in a SashLayer and is NOT part of a |
253 | | - // resize group: move the sash div by the pointer delta (pure arithmetic, |
254 | | - // zero getBoundingClientRect calls during drag). Geometry is re-synced |
| 254 | + // Fast path for direct drag feedback: move sash by pointer delta |
| 255 | + // (no getBoundingClientRect calls during drag). Geometry is re-synced |
255 | 256 | // once on mouse-up via requestLayout. |
256 | | - if (inLayer && !resizeGroup && resizerDiv) { |
| 257 | + if (resizerDiv) { |
257 | 258 | const dx = move.clientX - lastDragClientX; |
258 | 259 | const dy = move.clientY - lastDragClientY; |
259 | 260 | if (orientation === "horizontal") { |
|
319 | 320 | processPointerMove(); |
320 | 321 | cleanupPointerDragState(); |
321 | 322 | // Re-sync sash to exact geometry once at the end of drag. |
322 | | - if (inLayer) layerCtx?.requestLayout(); |
| 323 | + layerCtx.requestLayout(); |
323 | 324 | isResizing = false; |
324 | 325 | onResizing?.(false); |
325 | 326 | } |
|
406 | 407 | value.set(newValue); |
407 | 408 | updateDom(newValue, true); |
408 | 409 | reportWidth(newValue); |
409 | | - if (inLayer) layerCtx?.requestLayout(); |
| 410 | + layerCtx.requestLayout(); |
410 | 411 | } |
411 | 412 |
|
412 | 413 | $effect(() => { |
413 | 414 | if (resizeGroup && order !== undefined) { |
414 | 415 | // It's important we do not make use of maxValue in the resize |
415 | 416 | // manager, and in this effect. It changes with the value of |
416 | 417 | // neighbors and would make this effect trigger constantly. |
417 | | - return resizeGroup?.register({ |
| 418 | + return resizeGroup.register({ |
418 | 419 | resizerId, |
419 | 420 | getValue, |
420 | 421 | setValue, |
|
445 | 446 | } |
446 | 447 | }); |
447 | 448 |
|
448 | | - // Teleportation effect: move the resizer div into the SashLayer overlay so |
449 | | - // it is never clipped by overflow:hidden on pane containers. Position is |
| 449 | + // Overlay effect: move the resizer div into the SashLayer overlay so it is |
| 450 | + // never clipped by overflow:hidden on pane containers. Position is |
450 | 451 | // kept in sync via ResizeObserver + window resize + shared per-layer |
451 | 452 | // scheduler notifications. No scroll tracking is needed — the SashLayer is |
452 | 453 | // always scoped inside the same scroll container as the viewport, so |
453 | 454 | // getBoundingClientRect differences are scroll-invariant. |
454 | 455 | $effect(() => { |
455 | | - const container = layerCtx?.container; |
| 456 | + const container = layerCtx.container; |
456 | 457 | const div = resizerDiv; |
457 | 458 | const vp = viewport; |
458 | 459 | // Snapshot these so updatePosition closes over stable values. They are |
|
476 | 477 | ? parseFloat(vpStyle?.marginRight || "0") || 0 |
477 | 478 | : 0; |
478 | 479 |
|
479 | | - inLayer = true; |
480 | 480 | c.appendChild(d); |
481 | 481 |
|
482 | 482 | function updatePosition(containerRect?: DOMRectReadOnly) { |
|
520 | 520 |
|
521 | 521 | return () => { |
522 | 522 | if (pausedAutoLayout) { |
523 | | - layerCtx?.setAutoLayoutPaused(false); |
| 523 | + layerCtx.setAutoLayoutPaused(false); |
524 | 524 | pausedAutoLayout = false; |
525 | 525 | } |
526 | | - inLayer = false; |
527 | 526 | unobserveLayoutTarget?.(); |
528 | 527 | unsubscribeLayout?.(); |
529 | 528 | d.remove(); |
|
543 | 542 | {onclick} |
544 | 543 | class:disabled |
545 | 544 | class="resizer" |
546 | | - class:in-layer={inLayer} |
547 | 545 | class:is-resizing={isResizing} |
548 | 546 | class:vertical={orientation === "vertical"} |
549 | 547 | class:horizontal={orientation === "horizontal"} |
|
560 | 558 | --resizer-cursor: default; |
561 | 559 | position: absolute; |
562 | 560 | outline: none; |
563 | | - /* background-color: rgba(255, 0, 0, 0.345); */ |
564 | 561 | cursor: var(--resizer-cursor); |
| 562 | + /* background-color: rgba(255, 0, 0, 0.345); */ |
| 563 | + pointer-events: initial; |
565 | 564 |
|
566 | 565 | &.horizontal { |
567 | 566 | --resizer-cursor: col-resize; |
|
577 | 576 | height: var(--resizer-thickness); |
578 | 577 | } |
579 | 578 |
|
580 | | - /* Non-layer fallback positioning: when not teleported into SashLayer, |
581 | | - anchor to the requested viewport edge via directional classes. */ |
582 | | - &.horizontal.right:not(.in-layer) { |
| 579 | + /* Anchor-to-edge defaults before the first layout pass. */ |
| 580 | + &.horizontal.right { |
583 | 581 | right: 0; |
584 | 582 | left: auto; |
585 | 583 | } |
586 | 584 |
|
587 | | - &.horizontal.left:not(.in-layer) { |
| 585 | + &.horizontal.left { |
588 | 586 | right: auto; |
589 | 587 | left: 0; |
590 | 588 | } |
591 | 589 |
|
592 | | - &.vertical.down:not(.in-layer) { |
| 590 | + &.vertical.down { |
593 | 591 | top: auto; |
594 | 592 | bottom: 0; |
595 | 593 | } |
596 | 594 |
|
597 | | - &.vertical.up:not(.in-layer) { |
| 595 | + &.vertical.up { |
598 | 596 | top: 0; |
599 | 597 | bottom: auto; |
600 | 598 | } |
|
603 | 601 | pointer-events: none; |
604 | 602 | --resizer-cursor: default; |
605 | 603 | } |
606 | | -
|
607 | | - /* When teleported into the SashLayer overlay the container has |
608 | | - pointer-events:none, so individual resizers must opt back in. */ |
609 | | - &.in-layer { |
610 | | - pointer-events: initial; |
611 | | - } |
612 | | -
|
613 | | - &.in-layer.disabled { |
614 | | - pointer-events: none; |
615 | | - } |
616 | 604 | } |
617 | 605 | </style> |
0 commit comments