reshaped 4.2.0 → 4.3.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/bundle.css +1 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/components/Autocomplete/Autocomplete.js +1 -1
  4. package/dist/components/ContextMenu/tests/ContextMenu.stories.d.ts +7 -0
  5. package/dist/components/ContextMenu/tests/ContextMenu.stories.js +99 -1
  6. package/dist/components/DropdownMenu/DropdownMenu.js +1 -1
  7. package/dist/components/Flyout/Flyout.module.css +1 -1
  8. package/dist/components/Flyout/Flyout.types.d.ts +1 -0
  9. package/dist/components/Flyout/FlyoutContent.js +3 -2
  10. package/dist/components/Flyout/FlyoutControlled.js +23 -4
  11. package/dist/components/Flyout/tests/Flyout.stories.d.ts +7 -0
  12. package/dist/components/Flyout/tests/Flyout.stories.js +142 -1
  13. package/dist/components/Modal/Modal.js +13 -171
  14. package/dist/components/Modal/Modal.module.css +1 -1
  15. package/dist/components/Modal/Modal.types.d.ts +2 -1
  16. package/dist/components/Modal/useModalDrag.d.ts +21 -0
  17. package/dist/components/Modal/useModalDrag.js +256 -0
  18. package/dist/components/Overlay/Overlay.js +5 -4
  19. package/dist/components/Overlay/Overlay.module.css +1 -1
  20. package/dist/components/ScrollArea/ScrollArea.js +29 -16
  21. package/dist/components/ScrollArea/ScrollArea.module.css +1 -1
  22. package/dist/components/ScrollArea/ScrollArea.types.d.ts +1 -1
  23. package/dist/components/Select/SelectCustomControlled.js +1 -1
  24. package/dist/components/Select/tests/Select.stories.js +12 -1
  25. package/dist/components/_private/Freeze/Freeze.d.ts +4 -0
  26. package/dist/components/_private/Freeze/Freeze.js +46 -0
  27. package/dist/components/_private/Freeze/Freeze.module.css +1 -0
  28. package/dist/components/_private/Freeze/Freeze.types.d.ts +14 -0
  29. package/dist/components/_private/Freeze/Freeze.types.js +1 -0
  30. package/dist/components/_private/Freeze/index.d.ts +1 -0
  31. package/dist/components/_private/Freeze/index.js +1 -0
  32. package/dist/components/_private/Freeze/tests/Freeze.stories.d.ts +7 -0
  33. package/dist/components/_private/Freeze/tests/Freeze.stories.js +88 -0
  34. package/dist/hooks/_internal/useSingletonHotkeys.js +4 -0
  35. package/dist/hooks/tests/useHotkeys.stories.d.ts +3 -0
  36. package/dist/hooks/tests/useHotkeys.stories.js +18 -1
  37. package/dist/hooks/tests/useOnClickOutside.stories.d.ts +6 -0
  38. package/dist/hooks/tests/useOnClickOutside.stories.js +39 -1
  39. package/dist/hooks/useOnClickOutside.js +12 -12
  40. package/dist/utilities/animation.d.ts +17 -0
  41. package/dist/utilities/animation.js +73 -0
  42. package/package.json +3 -3
@@ -1 +1 @@
1
- .root{--rs-modal-container-width:100vw;--rs-modal-container-height:100vh;transition:var(--rs-duration-fast) var(--rs-easing-accelerate);transition-property:transform,opacity;background:var(--rs-color-background-elevation-overlay);color:var(--rs-color-foreground-neutral);box-shadow:var(--rs-shadow-overlay);will-change:transform,opacity;outline:none;overscroll-behavior:none}[data-rs-keyboard] .root:focus{outline:var(--rs-outline)}.root.--contained{--rs-modal-container-width:100%;--rs-modal-container-height:100%}.root{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);--rs-modal-size-m:var(--rs-modal-size-s);--rs-modal-size-l:var(--rs-modal-size-m);--rs-modal-size-xl:var(--rs-modal-size-l);--rs-modal-size:var(--rs-modal-size-s)}.--position-center{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:auto;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x8));max-height:none;margin:var(--rs-unit-x4);inset:0;position:relative;border-radius:var(--rs-radius-large);overflow:hidden;overscroll-behavior:auto;opacity:0;transform:translateY(var(--rs-unit-x4))}.--position-center.--active,[dir=rtl] .--position-center.--active{transform:translate(0);opacity:1}.--position-bottom{--rs-modal-size-s:auto;width:100%;height:var(--rs-modal-size);max-width:100%;max-height:calc(var(--rs-modal-container-height) - var(--rs-unit-x4));margin:0;margin-top:var(--rs-unit-x4);inset:0;inset-block-start:auto;transform:translateY(100%);position:absolute;border-radius:var(--rs-radius-large) var(--rs-radius-large) 0 0;overflow:auto;overscroll-behavior:none;opacity:1}.--position-bottom.--active,[dir=rtl] .--position-bottom.--active{transform:translateY(max(var(--rs-modal-drag,0px),0px))}.--position-start{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-end:var(--rs-unit-x4);inset:0;inset-inline-end:auto;transform:translate(-100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-start{transform:translate(100%)}.--position-start.--active,[dir=rtl] .--position-start.--active{transform:translate(min(var(--rs-modal-drag,0px),0px))}.--position-end{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-start:var(--rs-unit-x4);inset:0;inset-inline-start:auto;transform:translate(100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-end{transform:translate(-100%)}.--position-end.--active,[dir=rtl] .--position-end.--active{transform:translate(max(var(--rs-modal-drag,0px),0px))}.--position-full-screen{--rs-modal-size-s:100%;width:100%;height:100%;max-width:var(--rs-modal-container-width);max-height:100%;margin:0;inset:0;inset-block-start:auto;transform:translateY(var(--rs-unit-x4));position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:0}.--position-full-screen.--active,[dir=rtl] .--position-full-screen.--active{transform:translate(0);opacity:1}.--active,[dir=rtl] .--active{transition-timing-function:var(--rs-easing-decelerate)}.--dragging{transition:none}.--overflow-visible{overflow:visible}@media (--rs-viewport-m ){.root{--rs-modal-size:var(--rs-modal-size-m)}.--position-center--m{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:auto;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x8));max-height:none;margin:var(--rs-unit-x4);inset:0;position:relative;border-radius:var(--rs-radius-large);overflow:hidden;overscroll-behavior:auto;opacity:0;transform:translateY(var(--rs-unit-x4))}.--position-center--m.--active,[dir=rtl] .--position-center--m.--active{transform:translate(0);opacity:1}.--position-bottom--m{--rs-modal-size-s:auto;width:100%;height:var(--rs-modal-size);max-width:100%;max-height:calc(var(--rs-modal-container-height) - var(--rs-unit-x4));margin:0;margin-top:var(--rs-unit-x4);inset:0;inset-block-start:auto;transform:translateY(100%);position:absolute;border-radius:var(--rs-radius-large) var(--rs-radius-large) 0 0;overflow:auto;overscroll-behavior:none;opacity:1}.--position-bottom--m.--active,[dir=rtl] .--position-bottom--m.--active{transform:translateY(max(var(--rs-modal-drag,0px),0px))}.--position-start--m{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-end:var(--rs-unit-x4);inset:0;inset-inline-end:auto;transform:translate(-100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-start--m{transform:translate(100%)}.--position-start--m.--active,[dir=rtl] .--position-start--m.--active{transform:translate(min(var(--rs-modal-drag,0px),0px))}.--position-end--m{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-start:var(--rs-unit-x4);inset:0;inset-inline-start:auto;transform:translate(100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-end--m{transform:translate(-100%)}.--position-end--m.--active,[dir=rtl] .--position-end--m.--active{transform:translate(max(var(--rs-modal-drag,0px),0px))}.--position-full-screen--m{--rs-modal-size-s:100%;width:100%;height:100%;max-width:var(--rs-modal-container-width);max-height:100%;margin:0;inset:0;inset-block-start:auto;transform:translateY(var(--rs-unit-x4));position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:0}.--position-full-screen--m.--active,[dir=rtl] .--position-full-screen--m.--active{transform:translate(0);opacity:1}}@media (--rs-viewport-l ){.root{--rs-modal-size:var(--rs-modal-size-l)}.--position-center--l{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:auto;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x8));max-height:none;margin:var(--rs-unit-x4);inset:0;position:relative;border-radius:var(--rs-radius-large);overflow:hidden;overscroll-behavior:auto;opacity:0;transform:translateY(var(--rs-unit-x4))}.--position-center--l.--active,[dir=rtl] .--position-center--l.--active{transform:translate(0);opacity:1}.--position-bottom--l{--rs-modal-size-s:auto;width:100%;height:var(--rs-modal-size);max-width:100%;max-height:calc(var(--rs-modal-container-height) - var(--rs-unit-x4));margin:0;margin-top:var(--rs-unit-x4);inset:0;inset-block-start:auto;transform:translateY(100%);position:absolute;border-radius:var(--rs-radius-large) var(--rs-radius-large) 0 0;overflow:auto;overscroll-behavior:none;opacity:1}.--position-bottom--l.--active,[dir=rtl] .--position-bottom--l.--active{transform:translateY(max(var(--rs-modal-drag,0px),0px))}.--position-start--l{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-end:var(--rs-unit-x4);inset:0;inset-inline-end:auto;transform:translate(-100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-start--l{transform:translate(100%)}.--position-start--l.--active,[dir=rtl] .--position-start--l.--active{transform:translate(min(var(--rs-modal-drag,0px),0px))}.--position-end--l{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-start:var(--rs-unit-x4);inset:0;inset-inline-start:auto;transform:translate(100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-end--l{transform:translate(-100%)}.--position-end--l.--active,[dir=rtl] .--position-end--l.--active{transform:translate(max(var(--rs-modal-drag,0px),0px))}.--position-full-screen--l{--rs-modal-size-s:100%;width:100%;height:100%;max-width:var(--rs-modal-container-width);max-height:100%;margin:0;inset:0;inset-block-start:auto;transform:translateY(var(--rs-unit-x4));position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:0}.--position-full-screen--l.--active,[dir=rtl] .--position-full-screen--l.--active{transform:translate(0);opacity:1}}@media (--rs-viewport-xl ){.root{--rs-modal-size:var(--rs-modal-size-xl)}.--position-center--xl{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:auto;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x8));max-height:none;margin:var(--rs-unit-x4);inset:0;position:relative;border-radius:var(--rs-radius-large);overflow:hidden;overscroll-behavior:auto;opacity:0;transform:translateY(var(--rs-unit-x4))}.--position-center--xl.--active,[dir=rtl] .--position-center--xl.--active{transform:translate(0);opacity:1}.--position-bottom--xl{--rs-modal-size-s:auto;width:100%;height:var(--rs-modal-size);max-width:100%;max-height:calc(var(--rs-modal-container-height) - var(--rs-unit-x4));margin:0;margin-top:var(--rs-unit-x4);inset:0;inset-block-start:auto;transform:translateY(100%);position:absolute;border-radius:var(--rs-radius-large) var(--rs-radius-large) 0 0;overflow:auto;overscroll-behavior:none;opacity:1}.--position-bottom--xl.--active,[dir=rtl] .--position-bottom--xl.--active{transform:translateY(max(var(--rs-modal-drag,0px),0px))}.--position-start--xl{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-end:var(--rs-unit-x4);inset:0;inset-inline-end:auto;transform:translate(-100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-start--xl{transform:translate(100%)}.--position-start--xl.--active,[dir=rtl] .--position-start--xl.--active{transform:translate(min(var(--rs-modal-drag,0px),0px))}.--position-end--xl{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-start:var(--rs-unit-x4);inset:0;inset-inline-start:auto;transform:translate(100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-end--xl{transform:translate(-100%)}.--position-end--xl.--active,[dir=rtl] .--position-end--xl.--active{transform:translate(max(var(--rs-modal-drag,0px),0px))}.--position-full-screen--xl{--rs-modal-size-s:100%;width:100%;height:100%;max-width:var(--rs-modal-container-width);max-height:100%;margin:0;inset:0;inset-block-start:auto;transform:translateY(var(--rs-unit-x4));position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:0}.--position-full-screen--xl.--active,[dir=rtl] .--position-full-screen--xl.--active{transform:translate(0);opacity:1}}
1
+ @property --rs-modal-drag{syntax:"<length>";initial-value:0;inherits:false}.root{--rs-modal-container-width:100vw;--rs-modal-container-height:100vh;transition:var(--rs-duration-fast) var(--rs-easing-accelerate);transition-property:transform,opacity;background:var(--rs-color-background-elevation-overlay);color:var(--rs-color-foreground-neutral);box-shadow:var(--rs-shadow-overlay);will-change:transform,opacity;outline:none;overscroll-behavior:none}[data-rs-keyboard] .root:focus{outline:var(--rs-outline)}.root.--contained{--rs-modal-container-width:100%;--rs-modal-container-height:100%}.root{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);--rs-modal-size-m:var(--rs-modal-size-s);--rs-modal-size-l:var(--rs-modal-size-m);--rs-modal-size-xl:var(--rs-modal-size-l);--rs-modal-size:var(--rs-modal-size-s)}.--position-center{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:auto;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x8));max-height:none;margin:var(--rs-unit-x4);inset:0;position:relative;border-radius:var(--rs-radius-large);overflow:hidden;overscroll-behavior:auto;opacity:0;transform:translateY(var(--rs-unit-x4))}.--position-center.--active,[dir=rtl] .--position-center.--active{transform:translate(0);opacity:1}.--position-bottom{--rs-modal-size-s:auto;width:100%;height:var(--rs-modal-size);max-width:100%;max-height:calc(var(--rs-modal-container-height) - var(--rs-unit-x4));margin:0;margin-top:var(--rs-unit-x4);inset:0;inset-block-start:auto;transform:translateY(100%);position:absolute;border-radius:var(--rs-radius-large) var(--rs-radius-large) 0 0;overflow:auto;overscroll-behavior:none;opacity:1}.--position-bottom.--active,[dir=rtl] .--position-bottom.--active{transform:translateY(max(var(--rs-modal-drag,0px),0px))}.--position-start{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-end:var(--rs-unit-x4);inset:0;inset-inline-end:auto;transform:translate(-100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-start{transform:translate(100%)}.--position-start.--active,[dir=rtl] .--position-start.--active{transform:translate(min(var(--rs-modal-drag,0px),0px))}.--position-end{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-start:var(--rs-unit-x4);inset:0;inset-inline-start:auto;transform:translate(100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-end{transform:translate(-100%)}.--position-end.--active,[dir=rtl] .--position-end.--active{transform:translate(max(var(--rs-modal-drag,0px),0px))}.--position-full-screen{--rs-modal-size-s:100%;width:100%;height:100%;max-width:var(--rs-modal-container-width);max-height:100%;margin:0;inset:0;inset-block-start:auto;transform:translateY(var(--rs-unit-x4));position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:0}.--position-full-screen.--active,[dir=rtl] .--position-full-screen.--active{transform:translate(0);opacity:1}.--active,[dir=rtl] .--active{transition-timing-function:var(--rs-easing-decelerate)}.--dragging{transition:none}.--overflow-visible{overflow:visible}@media (--rs-viewport-m ){.root{--rs-modal-size:var(--rs-modal-size-m)}.--position-center--m{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:auto;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x8));max-height:none;margin:var(--rs-unit-x4);inset:0;position:relative;border-radius:var(--rs-radius-large);overflow:hidden;overscroll-behavior:auto;opacity:0;transform:translateY(var(--rs-unit-x4))}.--position-center--m.--active,[dir=rtl] .--position-center--m.--active{transform:translate(0);opacity:1}.--position-bottom--m{--rs-modal-size-s:auto;width:100%;height:var(--rs-modal-size);max-width:100%;max-height:calc(var(--rs-modal-container-height) - var(--rs-unit-x4));margin:0;margin-top:var(--rs-unit-x4);inset:0;inset-block-start:auto;transform:translateY(100%);position:absolute;border-radius:var(--rs-radius-large) var(--rs-radius-large) 0 0;overflow:auto;overscroll-behavior:none;opacity:1}.--position-bottom--m.--active,[dir=rtl] .--position-bottom--m.--active{transform:translateY(max(var(--rs-modal-drag,0px),0px))}.--position-start--m{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-end:var(--rs-unit-x4);inset:0;inset-inline-end:auto;transform:translate(-100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-start--m{transform:translate(100%)}.--position-start--m.--active,[dir=rtl] .--position-start--m.--active{transform:translate(min(var(--rs-modal-drag,0px),0px))}.--position-end--m{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-start:var(--rs-unit-x4);inset:0;inset-inline-start:auto;transform:translate(100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-end--m{transform:translate(-100%)}.--position-end--m.--active,[dir=rtl] .--position-end--m.--active{transform:translate(max(var(--rs-modal-drag,0px),0px))}.--position-full-screen--m{--rs-modal-size-s:100%;width:100%;height:100%;max-width:var(--rs-modal-container-width);max-height:100%;margin:0;inset:0;inset-block-start:auto;transform:translateY(var(--rs-unit-x4));position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:0}.--position-full-screen--m.--active,[dir=rtl] .--position-full-screen--m.--active{transform:translate(0);opacity:1}}@media (--rs-viewport-l ){.root{--rs-modal-size:var(--rs-modal-size-l)}.--position-center--l{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:auto;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x8));max-height:none;margin:var(--rs-unit-x4);inset:0;position:relative;border-radius:var(--rs-radius-large);overflow:hidden;overscroll-behavior:auto;opacity:0;transform:translateY(var(--rs-unit-x4))}.--position-center--l.--active,[dir=rtl] .--position-center--l.--active{transform:translate(0);opacity:1}.--position-bottom--l{--rs-modal-size-s:auto;width:100%;height:var(--rs-modal-size);max-width:100%;max-height:calc(var(--rs-modal-container-height) - var(--rs-unit-x4));margin:0;margin-top:var(--rs-unit-x4);inset:0;inset-block-start:auto;transform:translateY(100%);position:absolute;border-radius:var(--rs-radius-large) var(--rs-radius-large) 0 0;overflow:auto;overscroll-behavior:none;opacity:1}.--position-bottom--l.--active,[dir=rtl] .--position-bottom--l.--active{transform:translateY(max(var(--rs-modal-drag,0px),0px))}.--position-start--l{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-end:var(--rs-unit-x4);inset:0;inset-inline-end:auto;transform:translate(-100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-start--l{transform:translate(100%)}.--position-start--l.--active,[dir=rtl] .--position-start--l.--active{transform:translate(min(var(--rs-modal-drag,0px),0px))}.--position-end--l{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-start:var(--rs-unit-x4);inset:0;inset-inline-start:auto;transform:translate(100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-end--l{transform:translate(-100%)}.--position-end--l.--active,[dir=rtl] .--position-end--l.--active{transform:translate(max(var(--rs-modal-drag,0px),0px))}.--position-full-screen--l{--rs-modal-size-s:100%;width:100%;height:100%;max-width:var(--rs-modal-container-width);max-height:100%;margin:0;inset:0;inset-block-start:auto;transform:translateY(var(--rs-unit-x4));position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:0}.--position-full-screen--l.--active,[dir=rtl] .--position-full-screen--l.--active{transform:translate(0);opacity:1}}@media (--rs-viewport-xl ){.root{--rs-modal-size:var(--rs-modal-size-xl)}.--position-center--xl{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:auto;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x8));max-height:none;margin:var(--rs-unit-x4);inset:0;position:relative;border-radius:var(--rs-radius-large);overflow:hidden;overscroll-behavior:auto;opacity:0;transform:translateY(var(--rs-unit-x4))}.--position-center--xl.--active,[dir=rtl] .--position-center--xl.--active{transform:translate(0);opacity:1}.--position-bottom--xl{--rs-modal-size-s:auto;width:100%;height:var(--rs-modal-size);max-width:100%;max-height:calc(var(--rs-modal-container-height) - var(--rs-unit-x4));margin:0;margin-top:var(--rs-unit-x4);inset:0;inset-block-start:auto;transform:translateY(100%);position:absolute;border-radius:var(--rs-radius-large) var(--rs-radius-large) 0 0;overflow:auto;overscroll-behavior:none;opacity:1}.--position-bottom--xl.--active,[dir=rtl] .--position-bottom--xl.--active{transform:translateY(max(var(--rs-modal-drag,0px),0px))}.--position-start--xl{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-end:var(--rs-unit-x4);inset:0;inset-inline-end:auto;transform:translate(-100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-start--xl{transform:translate(100%)}.--position-start--xl.--active,[dir=rtl] .--position-start--xl.--active{transform:translate(min(var(--rs-modal-drag,0px),0px))}.--position-end--xl{--rs-modal-size-s:calc(var(--rs-unit-x1) * 100);width:var(--rs-modal-size);height:100%;max-width:calc(var(--rs-modal-container-width) - var(--rs-unit-x4));max-height:100%;margin:0;margin-inline-start:var(--rs-unit-x4);inset:0;inset-inline-start:auto;transform:translate(100%);position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:1}[dir=rtl] .--position-end--xl{transform:translate(-100%)}.--position-end--xl.--active,[dir=rtl] .--position-end--xl.--active{transform:translate(max(var(--rs-modal-drag,0px),0px))}.--position-full-screen--xl{--rs-modal-size-s:100%;width:100%;height:100%;max-width:var(--rs-modal-container-width);max-height:100%;margin:0;inset:0;inset-block-start:auto;transform:translateY(var(--rs-unit-x4));position:absolute;border-radius:0;overflow:auto;overscroll-behavior:none;opacity:0}.--position-full-screen--xl.--active,[dir=rtl] .--position-full-screen--xl.--active{transform:translate(0);opacity:1}}
@@ -3,6 +3,7 @@ import type { ClassName } from "@reshaped/utilities";
3
3
  import type { OverlayCloseReason, OverlayProps } from "../../components/Overlay";
4
4
  import type * as G from "../../types/global";
5
5
  import type { Attributes } from "../../types/global";
6
+ export type Position = "center" | "end" | "bottom" | "start" | "full-screen";
6
7
  export type Context = {
7
8
  id: string;
8
9
  titleMounted: boolean;
@@ -22,7 +23,7 @@ export type Props = {
22
23
  /** Node for inserting children */
23
24
  children?: React.ReactNode;
24
25
  /** Component position on the screen */
25
- position?: G.Responsive<"center" | "end" | "bottom" | "start" | "full-screen">;
26
+ position?: G.Responsive<Position>;
26
27
  /** Component size, literal css value */
27
28
  size?: G.Responsive<string>;
28
29
  /** Component padding, unit token multiplier */
@@ -0,0 +1,21 @@
1
+ import type { OverlayInstance } from "../../components/Overlay";
2
+ import type * as T from "./Modal.types";
3
+ /**
4
+ * Swipe gesture closing the modal, moving it with the finger and continuing
5
+ * the release animation from the velocity the gesture ended with
6
+ */
7
+ declare const useModalDrag: (args: {
8
+ position: T.Position;
9
+ rootRef: React.RefObject<HTMLDivElement | null>;
10
+ overlayRef: React.RefObject<OverlayInstance | null>;
11
+ active?: boolean;
12
+ disabled?: boolean;
13
+ transparentOverlay?: boolean;
14
+ containerRef?: T.Props["containerRef"];
15
+ onClose?: T.Props["onClose"];
16
+ }) => {
17
+ dragging: boolean;
18
+ handleTouchStart: (e: React.TouchEvent) => void;
19
+ handleTransitionEnd: (e: React.TransitionEvent) => void;
20
+ };
21
+ export default useModalDrag;
@@ -0,0 +1,256 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { enableScroll, disableScroll } from "@reshaped/utilities/internal";
4
+ import useHandlerRef from "../../hooks/useHandlerRef";
5
+ import useIsomorphicLayoutEffect from "../../hooks/useIsomorphicLayoutEffect";
6
+ import { resolveSpringTransition } from "../../utilities/animation";
7
+ /** Distance in px the modal has to be dragged before it starts following the finger */
8
+ const THRESHOLD = 32;
9
+ /** Distance in px on the axis perpendicular to the drag axis cancelling the gesture */
10
+ const OPPOSITE_THRESHOLD = 100;
11
+ /** Screen edge zone in px reserved for the browser back gesture */
12
+ const EDGE_BOUNDARY = 32;
13
+ /** Distance in px after which the modal content stops scrolling for the rest of the gesture */
14
+ const SCROLL_LOCK_THRESHOLD = 8;
15
+ /** Velocity in px/ms closing the modal on release regardless of the dragged distance */
16
+ const VELOCITY_THRESHOLD = 0.4;
17
+ /** Time window in ms used for measuring the release velocity */
18
+ const VELOCITY_WINDOW = 100;
19
+ const VELOCITY_SAMPLES = 10;
20
+ const getInitialState = () => ({
21
+ origin: 0,
22
+ oppositeOrigin: 0,
23
+ previous: 0,
24
+ seed: 0,
25
+ distance: 0,
26
+ offset: 0,
27
+ size: 1,
28
+ samples: [],
29
+ closing: false,
30
+ returning: false,
31
+ });
32
+ /**
33
+ * Distance in px the modal is currently rendered at from its open position, towards the closing direction.
34
+ * Resolved from the transform so a gesture can pick up an animation that is still in flight,
35
+ * the dragging class disables transitions and would otherwise snap the modal to the animation target
36
+ */
37
+ const getRenderedOffset = (el, args) => {
38
+ const { transform } = window.getComputedStyle(el);
39
+ if (!transform || transform === "none")
40
+ return 0;
41
+ const matrix = new DOMMatrixReadOnly(transform);
42
+ return Math.max(0, (args.axis === "y" ? matrix.m42 : matrix.m41) * args.closingSign);
43
+ };
44
+ /** Velocity in px/ms measured over the most recent samples, signed on the drag axis */
45
+ const getVelocity = (state, time) => {
46
+ const samples = state.samples.filter((sample) => time - sample.time <= VELOCITY_WINDOW);
47
+ const first = samples[0];
48
+ const last = samples[samples.length - 1];
49
+ if (!first || !last || first === last)
50
+ return 0;
51
+ return (last.coordinate - first.coordinate) / (last.time - first.time);
52
+ };
53
+ /**
54
+ * Swipe gesture closing the modal, moving it with the finger and continuing
55
+ * the release animation from the velocity the gesture ended with
56
+ */
57
+ const useModalDrag = (args) => {
58
+ const { position, rootRef, overlayRef, active, disabled, transparentOverlay, containerRef } = args;
59
+ const onCloseRef = useHandlerRef(args.onClose);
60
+ const [dragging, setDragging] = useState(false);
61
+ const stateRef = useRef(getInitialState());
62
+ const frameRef = useRef(null);
63
+ const axis = position === "bottom" ? "y" : "x";
64
+ // Start drawers close towards the negative coordinates
65
+ const closingSign = position === "start" ? -1 : 1;
66
+ const render = useCallback(() => {
67
+ const rootEl = rootRef.current;
68
+ const state = stateRef.current;
69
+ if (!rootEl)
70
+ return;
71
+ // A gesture picking up an animation starts on an already moving modal, so it skips the dead zone
72
+ const threshold = state.seed ? 0 : THRESHOLD;
73
+ const closingDistance = state.seed + state.distance;
74
+ state.offset = Math.max(0, closingDistance - threshold);
75
+ rootEl.style.setProperty("--rs-modal-drag", `${state.offset * closingSign}px`);
76
+ if (transparentOverlay)
77
+ return;
78
+ const progress = Math.min(1, closingDistance / state.size);
79
+ overlayRef.current?.setOpacity(1 - progress * 0.5);
80
+ }, [closingSign, transparentOverlay, rootRef, overlayRef]);
81
+ const reset = useCallback(() => {
82
+ stateRef.current = getInitialState();
83
+ render();
84
+ }, [render]);
85
+ const setScrollLock = useCallback((locked) => {
86
+ const style = rootRef.current?.style;
87
+ if (locked)
88
+ style?.setProperty("overflow", "hidden");
89
+ else
90
+ style?.removeProperty("overflow");
91
+ }, [rootRef]);
92
+ /**
93
+ * Continues the release animation from the current gesture velocity,
94
+ * velocity is passed in px/ms and is positive when moving towards the closing direction
95
+ */
96
+ const setReleaseTransition = useCallback((releaseArgs) => {
97
+ const rootEl = rootRef.current;
98
+ const { offset, size } = stateRef.current;
99
+ const displacement = offset - (releaseArgs.closing ? size : 0);
100
+ // Without a displacement no transition will run and the override would stay applied
101
+ if (!rootEl || !displacement)
102
+ return;
103
+ const { duration, easing } = resolveSpringTransition({
104
+ displacement,
105
+ velocity: releaseArgs.velocity,
106
+ });
107
+ rootEl.style.setProperty("transition-duration", `${Math.round(duration)}ms`);
108
+ rootEl.style.setProperty("transition-timing-function", easing);
109
+ }, [rootRef]);
110
+ const resetReleaseTransition = useCallback(() => {
111
+ const style = rootRef.current?.style;
112
+ style?.removeProperty("transition-duration");
113
+ style?.removeProperty("transition-timing-function");
114
+ }, [rootRef]);
115
+ const handleTouchStart = (e) => {
116
+ if (disabled || position === "center")
117
+ return;
118
+ // Prevent swipe to close from happening when user is working with text selection
119
+ if (window.getSelection()?.toString())
120
+ return;
121
+ const rootEl = rootRef.current;
122
+ const touch = e.targetTouches[0];
123
+ let currentEl = e.target;
124
+ while (currentEl && (currentEl === rootEl || rootEl?.contains(currentEl))) {
125
+ // Prioritize scrolling over modal swiping
126
+ if (currentEl.scrollTop !== 0 || currentEl.scrollLeft !== 0)
127
+ return;
128
+ // Start dragging only when starting on static elements
129
+ if (currentEl.matches("input,textarea"))
130
+ return;
131
+ currentEl = currentEl.parentElement;
132
+ }
133
+ // Prevent the drag handling when browser is trying to navigate to a previous page
134
+ if (position === "start" && touch.clientX < EDGE_BOUNDARY)
135
+ return;
136
+ // Measured before the state is replaced, a transition in flight resolves to its current position
137
+ const seed = active && rootEl ? getRenderedOffset(rootEl, { axis, closingSign }) : 0;
138
+ const origin = axis === "y" ? touch.clientY : touch.clientX;
139
+ // The previous gesture can be left unresolved, e.g. when the modal stays open after a drag close
140
+ stateRef.current = {
141
+ ...getInitialState(),
142
+ origin,
143
+ oppositeOrigin: axis === "y" ? touch.clientX : touch.clientY,
144
+ previous: origin,
145
+ seed,
146
+ size: rootEl ? Math.max(1, axis === "y" ? rootEl.clientHeight : rootEl.clientWidth) : 1,
147
+ };
148
+ resetReleaseTransition();
149
+ render();
150
+ if (containerRef?.current)
151
+ disableScroll();
152
+ setDragging(true);
153
+ };
154
+ const handleTransitionEnd = (e) => {
155
+ if (e.propertyName !== "transform")
156
+ return;
157
+ if (e.currentTarget !== e.target)
158
+ return;
159
+ resetReleaseTransition();
160
+ // Once modal is closed - reset its drag data
161
+ if (active)
162
+ return;
163
+ reset();
164
+ setScrollLock(false);
165
+ };
166
+ // Modal returns to its open position after the dragging class is removed,
167
+ // so the transition is applied to the reset instead of snapping back
168
+ useIsomorphicLayoutEffect(() => {
169
+ if (dragging || !stateRef.current.returning)
170
+ return;
171
+ reset();
172
+ }, [dragging, reset]);
173
+ useEffect(() => {
174
+ if (!dragging)
175
+ return;
176
+ const handleTouchMove = (e) => {
177
+ const state = stateRef.current;
178
+ const rootEl = rootRef.current;
179
+ if (rootEl?.scrollTop !== 0 || rootEl?.scrollLeft !== 0)
180
+ return;
181
+ const touch = e.targetTouches[0];
182
+ const coordinate = axis === "y" ? touch.clientY : touch.clientX;
183
+ const oppositeCoordinate = axis === "y" ? touch.clientX : touch.clientY;
184
+ const travel = Math.abs(coordinate - state.origin);
185
+ const oppositeTravel = Math.abs(oppositeCoordinate - state.oppositeOrigin);
186
+ // Positive when moving towards the closing direction
187
+ const delta = (coordinate - state.previous) * closingSign;
188
+ state.previous = coordinate;
189
+ // For start/end drawers - ignore the swiping
190
+ // If user is scrolling vertically more than swiping
191
+ if (axis === "x" && (travel < oppositeTravel || oppositeTravel > OPPOSITE_THRESHOLD)) {
192
+ return;
193
+ }
194
+ if (travel > SCROLL_LOCK_THRESHOLD && delta > 0)
195
+ setScrollLock(true);
196
+ // Clamped so the modal can't be dragged past its open position
197
+ state.distance = Math.max(-state.seed, state.distance + delta);
198
+ state.samples.push({ time: e.timeStamp, coordinate });
199
+ if (state.samples.length > VELOCITY_SAMPLES)
200
+ state.samples.shift();
201
+ if (frameRef.current !== null)
202
+ return;
203
+ frameRef.current = window.requestAnimationFrame(() => {
204
+ frameRef.current = null;
205
+ render();
206
+ });
207
+ };
208
+ const handleTouchEnd = (e) => {
209
+ const state = stateRef.current;
210
+ // Positive when moving towards the closing direction
211
+ const velocity = getVelocity(state, e.timeStamp) * closingSign;
212
+ const flicked = velocity > VELOCITY_THRESHOLD;
213
+ const dragged = state.distance > THRESHOLD && velocity >= 0;
214
+ state.closing = flicked || dragged;
215
+ state.returning = !state.closing;
216
+ // Apply the pending frame before the release so the transition starts from the current position
217
+ if (frameRef.current !== null) {
218
+ window.cancelAnimationFrame(frameRef.current);
219
+ frameRef.current = null;
220
+ render();
221
+ }
222
+ setReleaseTransition({ closing: state.closing, velocity });
223
+ if (containerRef?.current)
224
+ enableScroll();
225
+ if (state.returning)
226
+ setScrollLock(false);
227
+ setDragging(false);
228
+ if (state.closing)
229
+ onCloseRef.current?.({ reason: "drag" });
230
+ };
231
+ document.addEventListener("touchmove", handleTouchMove, { passive: true });
232
+ document.addEventListener("touchend", handleTouchEnd, { passive: true });
233
+ return () => {
234
+ if (frameRef.current !== null) {
235
+ window.cancelAnimationFrame(frameRef.current);
236
+ frameRef.current = null;
237
+ }
238
+ if (!stateRef.current.closing)
239
+ setScrollLock(false);
240
+ document.removeEventListener("touchmove", handleTouchMove);
241
+ document.removeEventListener("touchend", handleTouchEnd);
242
+ };
243
+ }, [
244
+ dragging,
245
+ axis,
246
+ closingSign,
247
+ containerRef,
248
+ rootRef,
249
+ onCloseRef,
250
+ render,
251
+ setScrollLock,
252
+ setReleaseTransition,
253
+ ]);
254
+ return { dragging, handleTouchStart, handleTransitionEnd };
255
+ };
256
+ export default useModalDrag;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { TrapFocus, classNames } from "@reshaped/utilities";
5
5
  import Portal from "../../components/_private/Portal";
@@ -21,6 +21,7 @@ const Overlay = (props) => {
21
21
  const [animated, setAnimated] = React.useState(false);
22
22
  const [offset, setOffset] = React.useState([0, 0]);
23
23
  const rootRef = React.useRef(null);
24
+ const backdropRef = React.useRef(null);
24
25
  const scopeRef = React.useRef(null);
25
26
  const contentRef = React.useRef(null);
26
27
  const { lockScroll, unlockScroll } = useScrollLock({ containerRef });
@@ -125,7 +126,8 @@ const Overlay = (props) => {
125
126
  setOpacity: (value) => {
126
127
  if (transparent)
127
128
  return;
128
- rootRef.current?.style.setProperty("--rs-overlay-opacity", `${value}`);
129
+ // Applied directly to the backdrop element to keep the style recalculation scoped to it
130
+ backdropRef.current?.style.setProperty("opacity", `${value}`);
129
131
  },
130
132
  }), [transparent]);
131
133
  if (!rendered || !mounted)
@@ -136,10 +138,9 @@ const Overlay = (props) => {
136
138
  }, style: {
137
139
  "--rs-overlay-offset-x": containerRef ? `${offset[0]}px` : undefined,
138
140
  "--rs-overlay-offset-y": containerRef ? `${offset[1]}px` : undefined,
139
- "--rs-overlay-opacity": transparent ? 0 : undefined,
140
141
  },
141
142
  // oxlint-disable-next-line jsx_a11y/prefer-tag-over-role
142
- role: "button", tabIndex: -1, className: rootClassNames, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onTransitionEnd: handleTransitionEnd, children: _jsx("div", { className: s.wrapper, children: _jsx("div", { className: s.inner, children: _jsx("div", { className: s.content, ref: contentRef, children: typeof children === "function" ? children({ active: visible }) : children }) }) }) })) }) }));
143
+ role: "button", tabIndex: -1, className: rootClassNames, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onTransitionEnd: handleTransitionEnd, children: _jsxs("div", { className: s.wrapper, children: [_jsx("div", { className: s.backdrop, ref: backdropRef }), _jsx("div", { className: s.inner, children: _jsx("div", { className: s.content, ref: contentRef, children: typeof children === "function" ? children({ active: visible }) : children }) })] }) })) }) }));
143
144
  };
144
145
  Overlay.displayName = "Overlay";
145
146
  export default Overlay;
@@ -1 +1 @@
1
- .root{--rs-overlay-offset-y:0px;--rs-overlay-offset-x:0px;--rs-overlay-opacity:0;position:fixed;overflow-x:clip;-webkit-overflow-scrolling:touch;left:var(--rs-overlay-offset-x);top:var(--rs-overlay-offset-y);z-index:var(--rs-z-index-fixed);color:var(--rs-color-white);background-color:rgb(from var(--rs-color-black) r g b/0);opacity:0;outline:none;isolation:isolate}.root,.wrapper{height:100%;width:100%}.wrapper{display:table}.inner{display:table-cell;text-align:center}.content,.inner{vertical-align:middle}.content{display:inline-block;text-align:initial}.root.--visible{--rs-overlay-opacity:1;background-color:rgb(from var(--rs-color-black) r g b/calc(var(--rs-overlay-opacity) * .7));opacity:1}.root.--click-through{color:inherit;pointer-events:none}.root.--blurred{backdrop-filter:blur(3px);left:calc(var(--rs-overlay-offset-x) - 1px);top:calc(var(--rs-overlay-offset-y) - 1px);width:calc(100% + 2px);height:calc(100% + 2px)}.root.--click-through .content,.root.--click-through>:not(.wrapper){pointer-events:all}.root.--contained{position:absolute}.root.--animated{transition:var(--rs-duration-medium) var(--rs-easing-accelerate);transition-property:background-color,opacity}.root.--animated.--visible{transition-timing-function:var(--rs-easing-decelerate)}.root.--overflow-auto{overflow:auto;overscroll-behavior:none}
1
+ .root{--rs-overlay-offset-y:0px;--rs-overlay-offset-x:0px;position:fixed;overflow-x:clip;-webkit-overflow-scrolling:touch;left:var(--rs-overlay-offset-x);top:var(--rs-overlay-offset-y);z-index:var(--rs-z-index-fixed);color:var(--rs-color-white);opacity:0;outline:none;isolation:isolate}.root,.wrapper{height:100%;width:100%}.wrapper{display:table;position:relative}.backdrop{position:absolute;inset:0;z-index:-1;background-color:rgb(from var(--rs-color-black) r g b/70%);opacity:0;will-change:opacity}.inner{display:table-cell;text-align:center}.content,.inner{vertical-align:middle}.content{display:inline-block;text-align:initial}.root.--visible,.root.--visible .backdrop{opacity:1}.root.--click-through{color:inherit;pointer-events:none}.root.--click-through .backdrop{opacity:0}.root.--blurred{backdrop-filter:blur(3px);left:calc(var(--rs-overlay-offset-x) - 1px);top:calc(var(--rs-overlay-offset-y) - 1px);width:calc(100% + 2px);height:calc(100% + 2px)}.root.--click-through .content,.root.--click-through>:not(.wrapper){pointer-events:all}.root.--contained{position:absolute}.root.--animated,.root.--animated .backdrop{transition:opacity var(--rs-duration-medium) var(--rs-easing-accelerate)}.root.--animated.--visible,.root.--animated.--visible .backdrop{transition-timing-function:var(--rs-easing-decelerate)}.root.--overflow-auto{overflow:auto;overscroll-behavior:none}
@@ -8,7 +8,7 @@ import useIsomorphicLayoutEffect from "../../hooks/useIsomorphicLayoutEffect";
8
8
  import { resolveMixin } from "../../styles/mixin";
9
9
  import s from "./ScrollArea.module.css";
10
10
  const ScrollAreaBar = (props) => {
11
- const { ratio, position, vertical, onThumbMove } = props;
11
+ const { ratio, vertical, scrollableRef, onThumbMove } = props;
12
12
  const onThumbMoveRef = useHandlerRef(onThumbMove);
13
13
  const [dragging, setDragging] = React.useState(false);
14
14
  const dragStartPositionRef = React.useRef(0);
@@ -49,6 +49,31 @@ const ScrollAreaBar = (props) => {
49
49
  setDragging(false);
50
50
  enableScroll();
51
51
  }, []);
52
+ /**
53
+ * Thumb position is written directly to a css variable on the bar instead of the react state,
54
+ * so scrolling doesn't re-render the component and the style recalculation stays scoped to the bar
55
+ */
56
+ useIsomorphicLayoutEffect(() => {
57
+ const scrollableEl = scrollableRef.current;
58
+ const barEl = barRef.current;
59
+ if (!scrollableEl || !barEl)
60
+ return;
61
+ const updatePosition = () => {
62
+ const { scrollLeft, scrollTop, clientWidth, clientHeight, scrollWidth, scrollHeight } = scrollableEl;
63
+ const position = vertical
64
+ ? scrollHeight <= clientHeight
65
+ ? 0
66
+ : scrollTop / scrollHeight
67
+ : scrollWidth <= clientWidth
68
+ ? 0
69
+ : scrollLeft / scrollWidth;
70
+ barEl.style.setProperty("--rs-scroll-area-position", `${position}`);
71
+ };
72
+ updatePosition();
73
+ scrollableEl.addEventListener("scroll", updatePosition, { passive: true });
74
+ return () => scrollableEl.removeEventListener("scroll", updatePosition);
75
+ // Ratio is included to re-sync the position after the content or the container size changes
76
+ }, [vertical, scrollableRef, ratio]);
52
77
  React.useEffect(() => {
53
78
  if (!dragging)
54
79
  return;
@@ -59,15 +84,11 @@ const ScrollAreaBar = (props) => {
59
84
  document.removeEventListener("mouseup", handleDragEnd);
60
85
  };
61
86
  }, [handleDrag, handleDragEnd, dragging]);
62
- return (_jsx("div", { className: barClassNames, style: {
63
- "--rs-scroll-area-ratio": ratio,
64
- "--rs-scroll-area-position": position,
65
- }, ref: barRef, onClick: handleClick, onMouseDown: handleDragStart, "aria-hidden": "true", children: _jsx("div", { className: s.thumb }) }));
87
+ return (_jsx("div", { className: barClassNames, style: { "--rs-scroll-area-ratio": ratio }, ref: barRef, onClick: handleClick, onMouseDown: handleDragStart, "aria-hidden": "true", children: _jsx("div", { className: s.thumb }) }));
66
88
  };
67
89
  const ScrollArea = forwardRef((props, ref) => {
68
90
  const { children, height, maxHeight, orientation = "both", scrollbarDisplay = "hover", overscrollBehavior = "auto", fade, onScroll, className, attributes, scrollableAttributes, scrollableClassName, } = props;
69
91
  const [scrollRatio, setScrollRatio] = React.useState({ x: 1, y: 1 });
70
- const [scrollPosition, setScrollPosition] = React.useState({ x: 0, y: 0 });
71
92
  const [scrolling, setScrolling] = React.useState(false);
72
93
  const scrollingTimeoutRef = React.useRef(undefined);
73
94
  const scrollableRef = React.useRef(null);
@@ -80,15 +101,11 @@ const ScrollArea = forwardRef((props, ref) => {
80
101
  const scrollableEl = scrollableRef.current;
81
102
  if (!scrollableEl)
82
103
  return;
83
- const { scrollLeft, scrollTop, clientWidth, clientHeight, scrollWidth, scrollHeight } = scrollableEl;
104
+ const { clientWidth, clientHeight, scrollWidth, scrollHeight } = scrollableEl;
84
105
  setScrollRatio({
85
106
  x: scrollWidth === 0 ? 1 : Math.min(clientWidth / scrollWidth, 1),
86
107
  y: scrollHeight === 0 ? 1 : Math.min(clientHeight / scrollHeight, 1),
87
108
  });
88
- setScrollPosition({
89
- x: scrollWidth <= clientWidth ? 0 : scrollLeft / scrollWidth,
90
- y: scrollHeight <= clientHeight ? 0 : scrollTop / scrollHeight,
91
- });
92
109
  }, []);
93
110
  const handleScroll = (e) => {
94
111
  const { scrollLeft, scrollTop, clientWidth, clientHeight, scrollWidth, scrollHeight } = e.currentTarget;
@@ -97,10 +114,6 @@ const ScrollArea = forwardRef((props, ref) => {
97
114
  clearTimeout(scrollingTimeoutRef.current);
98
115
  scrollingTimeoutRef.current = setTimeout(() => setScrolling(false), 1000);
99
116
  }
100
- setScrollPosition({
101
- x: scrollLeft / scrollWidth,
102
- y: scrollTop / scrollHeight,
103
- });
104
117
  onScroll?.({
105
118
  x: scrollWidth === clientWidth ? 0 : scrollLeft / (scrollWidth - clientWidth),
106
119
  y: scrollHeight === clientHeight ? 0 : scrollTop / (scrollHeight - clientHeight),
@@ -161,7 +174,7 @@ const ScrollArea = forwardRef((props, ref) => {
161
174
  mutationObserver.disconnect();
162
175
  };
163
176
  }, [updateScroll]);
164
- return (_jsxs("div", { ...attributes, ref: rootRef, className: rootClassNames, style: mixinStyles.variables, children: [_jsx("div", { className: scrollableClassNames, ref: scrollableRef, onScroll: handleScroll, children: _jsx("div", { ...scrollableAttributes, className: classNames(s.content, scrollableClassName), ref: contentRef, children: children }) }), orientation !== "horizontal" && scrollRatio.y < 1 && scrollbarDisplay !== "hidden" && (_jsx(ScrollAreaBar, { vertical: true, onThumbMove: handleThumbYMove, ratio: scrollRatio.y, position: scrollPosition.y })), orientation !== "vertical" && scrollRatio.x < 1 && scrollbarDisplay !== "hidden" && (_jsx(ScrollAreaBar, { onThumbMove: handleThumbXMove, ratio: scrollRatio.x, position: scrollPosition.x }))] }));
177
+ return (_jsxs("div", { ...attributes, ref: rootRef, className: rootClassNames, style: mixinStyles.variables, children: [_jsx("div", { className: scrollableClassNames, ref: scrollableRef, onScroll: handleScroll, children: _jsx("div", { ...scrollableAttributes, className: classNames(s.content, scrollableClassName), ref: contentRef, children: children }) }), orientation !== "horizontal" && scrollRatio.y < 1 && scrollbarDisplay !== "hidden" && (_jsx(ScrollAreaBar, { vertical: true, onThumbMove: handleThumbYMove, ratio: scrollRatio.y, scrollableRef: scrollableRef })), orientation !== "vertical" && scrollRatio.x < 1 && scrollbarDisplay !== "hidden" && (_jsx(ScrollAreaBar, { onThumbMove: handleThumbXMove, ratio: scrollRatio.x, scrollableRef: scrollableRef }))] }));
165
178
  });
166
179
  ScrollArea.displayName = "ScrollArea";
167
180
  export default ScrollArea;
@@ -1 +1 @@
1
- @property --rs-scroll-area-fade-block-start{syntax:"<length>";initial-value:0;inherits:false}@property --rs-scroll-area-fade-block-end{syntax:"<length>";initial-value:0;inherits:false}@property --rs-scroll-area-fade-inline-start{syntax:"<length>";initial-value:0;inherits:false}@property --rs-scroll-area-fade-inline-end{syntax:"<length>";initial-value:0;inherits:false}.root{--rs-scroll-area-thumb-size:var(--rs-unit-x1);--rs-scroll-area-thumb-offset:var(--rs-unit-x1);--rs-scroll-area-fade-size:var(--rs-unit-x8);overflow:hidden;height:100%;position:relative}[data-rs-keyboard] .root:has(.scrollable:focus){outline:var(--rs-outline)}.scrollable{height:100%;position:relative;overflow:auto;scrollbar-width:none;outline:none;-webkit-overflow-scrolling:touch}.scrollable::-webkit-scrollbar{display:none;width:0;height:0}.--orientation-vertical{overflow-x:hidden}.--orientation-vertical>.content{max-width:100%}.--orientation-horizontal{overflow-y:hidden}.--orientation-horizontal>.content{max-height:100%}@keyframes fade-block-start{0%{--rs-scroll-area-fade-block-start:0px}to{--rs-scroll-area-fade-block-start:var(--rs-scroll-area-fade-size)}}@keyframes fade-block-end{0%{--rs-scroll-area-fade-block-end:var(--rs-scroll-area-fade-size)}to{--rs-scroll-area-fade-block-end:0px}}@keyframes fade-inline-start{0%{--rs-scroll-area-fade-inline-start:0px}to{--rs-scroll-area-fade-inline-start:var(--rs-scroll-area-fade-size)}}@keyframes fade-inline-end{0%{--rs-scroll-area-fade-inline-end:var(--rs-scroll-area-fade-size)}to{--rs-scroll-area-fade-inline-end:0px}}@supports (animation-timeline:scroll(self block)){.--fade{--rs-scroll-area-fade-inline-direction:to right;mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.1) calc(var(--rs-scroll-area-fade-block-start) * .2),rgba(0,0,0,.35) calc(var(--rs-scroll-area-fade-block-start) * .4),rgba(0,0,0,.65) calc(var(--rs-scroll-area-fade-block-start) * .6),rgba(0,0,0,.9) calc(var(--rs-scroll-area-fade-block-start) * .8),#000 var(--rs-scroll-area-fade-block-start),#000 calc(100% - var(--rs-scroll-area-fade-block-end)),rgba(0,0,0,.9) calc(100% - var(--rs-scroll-area-fade-block-end) * .8),rgba(0,0,0,.65) calc(100% - var(--rs-scroll-area-fade-block-end) * .6),rgba(0,0,0,.35) calc(100% - var(--rs-scroll-area-fade-block-end) * .4),rgba(0,0,0,.1) calc(100% - var(--rs-scroll-area-fade-block-end) * .2),transparent 100%),linear-gradient(var(--rs-scroll-area-fade-inline-direction),transparent 0,rgba(0,0,0,.1) calc(var(--rs-scroll-area-fade-inline-start) * .2),rgba(0,0,0,.35) calc(var(--rs-scroll-area-fade-inline-start) * .4),rgba(0,0,0,.65) calc(var(--rs-scroll-area-fade-inline-start) * .6),rgba(0,0,0,.9) calc(var(--rs-scroll-area-fade-inline-start) * .8),#000 var(--rs-scroll-area-fade-inline-start),#000 calc(100% - var(--rs-scroll-area-fade-inline-end)),rgba(0,0,0,.9) calc(100% - var(--rs-scroll-area-fade-inline-end) * .8),rgba(0,0,0,.65) calc(100% - var(--rs-scroll-area-fade-inline-end) * .6),rgba(0,0,0,.35) calc(100% - var(--rs-scroll-area-fade-inline-end) * .4),rgba(0,0,0,.1) calc(100% - var(--rs-scroll-area-fade-inline-end) * .2),transparent 100%);mask-composite:intersect;animation-name:fade-block-start,fade-block-end,fade-inline-start,fade-inline-end;animation-timing-function:linear;animation-fill-mode:both;animation-timeline:scroll(self block),scroll(self block),scroll(self inline),scroll(self inline);animation-range:0 var(--rs-scroll-area-fade-size),calc(100% - var(--rs-scroll-area-fade-size)) 100%,0 var(--rs-scroll-area-fade-size),calc(100% - var(--rs-scroll-area-fade-size)) 100%}[dir=rtl] .--fade{--rs-scroll-area-fade-inline-direction:to left}}.--overscroll-auto{overscroll-behavior:auto}.--overscroll-contain{overscroll-behavior:contain}.content{display:inline-block;vertical-align:top;min-width:100%;min-height:100%;height:100%}.scrollbar{position:absolute;box-sizing:initial;padding:var(--rs-scroll-area-thumb-offset)}.thumb{position:relative;height:100%;width:100%;background-color:var(--rs-color-background-neutral-faded);opacity:0}.thumb,.thumb:before{border-radius:var(--rs-radius-circular);transition:opacity var(--rs-duration-fast) var(--rs-easing-standard)}.thumb:before{content:"";display:block;background:var(--rs-color-foreground-neutral);position:absolute;opacity:.2}.--scrollbar-y{inset-block:0;inset-inline-end:0;width:var(--rs-scroll-area-thumb-size);padding-block:calc(var(--rs-scroll-area-thumb-offset) + var(--rs-unit-x1))}.--scrollbar-y .thumb:before{inset-block-start:calc(var(--rs-scroll-area-position) * 100%);width:100%;height:calc(var(--rs-scroll-area-ratio) * 100%)}.--scrollbar-x{inset-inline:0;inset-block-end:0;height:var(--rs-scroll-area-thumb-size);padding-inline:calc(var(--rs-scroll-area-thumb-offset) + var(--rs-unit-x0-5))}.--scrollbar-x .thumb:before{inset-inline-start:calc(var(--rs-scroll-area-position) * 100%);height:100%;width:calc(var(--rs-scroll-area-ratio) * 100%)}.--scrollbar-y:not(:last-child){inset-block-end:calc(var(--rs-scroll-area-thumb-size) + var(--rs-scroll-area-thumb-offset) * 2)}.--scrollbar-y~.--scrollbar-x{inset-inline-end:calc(var(--rs-scroll-area-thumb-size) + var(--rs-scroll-area-thumb-offset) * 2)}.--display-hover:hover>.scrollbar .thumb,.--display-hover>.--scrollbar-dragging .thumb,.--display-scroll.--scrolling>.scrollbar .thumb,.--display-scroll>.--scrollbar-dragging .thumb,.--display-visible>.scrollbar .thumb{opacity:1}.--display-hover:hover .--scrollbar-dragging .thumb:before,.--scrollbar-dragging .thumb:before,.scrollbar:hover .thumb:before{opacity:.32}
1
+ @property --rs-scroll-area-fade-block-start{syntax:"<length>";initial-value:0;inherits:false}@property --rs-scroll-area-fade-block-end{syntax:"<length>";initial-value:0;inherits:false}@property --rs-scroll-area-fade-inline-start{syntax:"<length>";initial-value:0;inherits:false}@property --rs-scroll-area-fade-inline-end{syntax:"<length>";initial-value:0;inherits:false}.root{--rs-scroll-area-thumb-size:var(--rs-unit-x1);--rs-scroll-area-thumb-offset:var(--rs-unit-x1);--rs-scroll-area-fade-size:var(--rs-unit-x8);overflow:hidden;height:100%;position:relative}[data-rs-keyboard] .root:has(.scrollable:focus){outline:var(--rs-outline)}.scrollable{height:100%;position:relative;overflow:auto;scrollbar-width:none;outline:none;-webkit-overflow-scrolling:touch}.scrollable::-webkit-scrollbar{display:none;width:0;height:0}.--orientation-vertical{overflow-x:hidden}.--orientation-vertical>.content{max-width:100%}.--orientation-horizontal{overflow-y:hidden}.--orientation-horizontal>.content{max-height:100%}@keyframes fade-block-start{0%{--rs-scroll-area-fade-block-start:0px}to{--rs-scroll-area-fade-block-start:var(--rs-scroll-area-fade-size)}}@keyframes fade-block-end{0%{--rs-scroll-area-fade-block-end:var(--rs-scroll-area-fade-size)}to{--rs-scroll-area-fade-block-end:0px}}@keyframes fade-inline-start{0%{--rs-scroll-area-fade-inline-start:0px}to{--rs-scroll-area-fade-inline-start:var(--rs-scroll-area-fade-size)}}@keyframes fade-inline-end{0%{--rs-scroll-area-fade-inline-end:var(--rs-scroll-area-fade-size)}to{--rs-scroll-area-fade-inline-end:0px}}@supports (animation-timeline:scroll(self block)){.--fade{--rs-scroll-area-fade-inline-direction:to right;mask-image:linear-gradient(to bottom,transparent 0,rgba(0,0,0,.1) calc(var(--rs-scroll-area-fade-block-start) * .2),rgba(0,0,0,.35) calc(var(--rs-scroll-area-fade-block-start) * .4),rgba(0,0,0,.65) calc(var(--rs-scroll-area-fade-block-start) * .6),rgba(0,0,0,.9) calc(var(--rs-scroll-area-fade-block-start) * .8),#000 var(--rs-scroll-area-fade-block-start),#000 calc(100% - var(--rs-scroll-area-fade-block-end)),rgba(0,0,0,.9) calc(100% - var(--rs-scroll-area-fade-block-end) * .8),rgba(0,0,0,.65) calc(100% - var(--rs-scroll-area-fade-block-end) * .6),rgba(0,0,0,.35) calc(100% - var(--rs-scroll-area-fade-block-end) * .4),rgba(0,0,0,.1) calc(100% - var(--rs-scroll-area-fade-block-end) * .2),transparent 100%),linear-gradient(var(--rs-scroll-area-fade-inline-direction),transparent 0,rgba(0,0,0,.1) calc(var(--rs-scroll-area-fade-inline-start) * .2),rgba(0,0,0,.35) calc(var(--rs-scroll-area-fade-inline-start) * .4),rgba(0,0,0,.65) calc(var(--rs-scroll-area-fade-inline-start) * .6),rgba(0,0,0,.9) calc(var(--rs-scroll-area-fade-inline-start) * .8),#000 var(--rs-scroll-area-fade-inline-start),#000 calc(100% - var(--rs-scroll-area-fade-inline-end)),rgba(0,0,0,.9) calc(100% - var(--rs-scroll-area-fade-inline-end) * .8),rgba(0,0,0,.65) calc(100% - var(--rs-scroll-area-fade-inline-end) * .6),rgba(0,0,0,.35) calc(100% - var(--rs-scroll-area-fade-inline-end) * .4),rgba(0,0,0,.1) calc(100% - var(--rs-scroll-area-fade-inline-end) * .2),transparent 100%);mask-composite:intersect;animation-name:fade-block-start,fade-block-end,fade-inline-start,fade-inline-end;animation-timing-function:linear;animation-fill-mode:both;animation-timeline:scroll(self block),scroll(self block),scroll(self inline),scroll(self inline);animation-range:0 var(--rs-scroll-area-fade-size),calc(100% - var(--rs-scroll-area-fade-size)) 100%,0 var(--rs-scroll-area-fade-size),calc(100% - var(--rs-scroll-area-fade-size)) 100%}[dir=rtl] .--fade{--rs-scroll-area-fade-inline-direction:to left}}.--overscroll-auto{overscroll-behavior:auto}.--overscroll-contain{overscroll-behavior:contain}.content{display:inline-block;vertical-align:top;min-width:100%;min-height:100%;height:100%}.scrollbar{position:absolute;box-sizing:initial;padding:var(--rs-scroll-area-thumb-offset)}.thumb{position:relative;height:100%;width:100%;background-color:var(--rs-color-background-neutral-faded);opacity:0}.thumb,.thumb:before{border-radius:var(--rs-radius-circular);transition:opacity var(--rs-duration-fast) var(--rs-easing-standard)}.thumb:before{content:"";display:block;background:var(--rs-color-foreground-neutral);position:absolute;opacity:.2;will-change:transform}.--scrollbar-y{inset-block:0;inset-inline-end:0;width:var(--rs-scroll-area-thumb-size);padding-block:calc(var(--rs-scroll-area-thumb-offset) + var(--rs-unit-x1))}.--scrollbar-y .thumb:before{inset-block-start:0;width:100%;height:calc(var(--rs-scroll-area-ratio) * 100%);transform:translateY(calc(var(--rs-scroll-area-position, 0) / var(--rs-scroll-area-ratio) * 100%))}.--scrollbar-x{inset-inline:0;inset-block-end:0;height:var(--rs-scroll-area-thumb-size);padding-inline:calc(var(--rs-scroll-area-thumb-offset) + var(--rs-unit-x0-5))}.--scrollbar-x .thumb:before{inset-inline-start:0;height:100%;width:calc(var(--rs-scroll-area-ratio) * 100%);transform:translateX(calc(var(--rs-scroll-area-position, 0) / var(--rs-scroll-area-ratio) * 100%))}.--scrollbar-y:not(:last-child){inset-block-end:calc(var(--rs-scroll-area-thumb-size) + var(--rs-scroll-area-thumb-offset) * 2)}.--scrollbar-y~.--scrollbar-x{inset-inline-end:calc(var(--rs-scroll-area-thumb-size) + var(--rs-scroll-area-thumb-offset) * 2)}.--display-hover:hover>.scrollbar .thumb,.--display-hover>.--scrollbar-dragging .thumb,.--display-scroll.--scrolling>.scrollbar .thumb,.--display-scroll>.--scrollbar-dragging .thumb,.--display-visible>.scrollbar .thumb{opacity:1}.--display-hover:hover .--scrollbar-dragging .thumb:before,.--scrollbar-dragging .thumb:before,.scrollbar:hover .thumb:before{opacity:.32}
@@ -31,8 +31,8 @@ export type Props = {
31
31
  };
32
32
  export type BarProps = {
33
33
  ratio: number;
34
- position: number;
35
34
  vertical?: boolean;
35
+ scrollableRef: React.RefObject<HTMLElement | null>;
36
36
  onThumbMove: (args: {
37
37
  value: number;
38
38
  type: "absolute" | "relative";
@@ -124,7 +124,7 @@ const SelectCustomControlled = (props) => {
124
124
  return passedRenderTrigger(attributes, { ...triggerState, value });
125
125
  return passedRenderTrigger(attributes, { ...triggerState, value });
126
126
  };
127
- return (_jsxs(DropdownMenu, { width: width, disableHideAnimation: true, position: position ?? "bottom", fallbackPositions: fallbackPositions ?? (position ? undefined : ["bottom", "top"]), fallbackAdjustLayout: !openedWithTouch, fallbackMinHeight: "220px", initialFocusRef: initialFocusRef, positionRef: positionRef, onClose: () => setOpenedWithTouch(false), children: [_jsx(DropdownMenu.Trigger, { children: (attributes) => {
127
+ return (_jsxs(DropdownMenu, { width: width, position: position ?? "bottom", fallbackPositions: fallbackPositions ?? (position ? undefined : ["bottom", "top"]), fallbackAdjustLayout: !openedWithTouch, fallbackMinHeight: "220px", initialFocusRef: initialFocusRef, positionRef: positionRef, onClose: () => setOpenedWithTouch(false), children: [_jsx(DropdownMenu.Trigger, { children: (attributes) => {
128
128
  const triggerAttributes = {
129
129
  ...attributes,
130
130
  onTouchStart: () => {
@@ -99,6 +99,10 @@ exports.base = {
99
99
  (0, test_1.expect)(options[0]).toHaveTextContent("Dog");
100
100
  (0, test_1.expect)(options[1]).toHaveTextContent("Turtle");
101
101
  await test_1.userEvent.click(document.body);
102
+ // Wait for the previous menu to finish its hide animation
103
+ await (0, test_1.waitFor)(() => {
104
+ (0, test_1.expect)((0, test_1.within)(canvasElement.ownerDocument.body).queryAllByRole("option")).toHaveLength(0);
105
+ });
102
106
  const [_, triggerWithGroups] = canvas.getAllByRole("button");
103
107
  const hiddenInputs2 = canvasElement.querySelectorAll('input[type="hidden"]');
104
108
  const [__, hiddenInputWithGroups] = Array.from(hiddenInputs2);
@@ -345,7 +349,10 @@ exports.renderTrigger = {
345
349
  await test_1.userEvent.click(option);
346
350
  (0, test_1.expect)(hiddenInput).toHaveValue("turtle");
347
351
  (0, test_1.expect)(trigger).toHaveTextContent("Turtle");
348
- (0, test_1.expect)(trigger).toHaveAttribute("aria-expanded", "false");
352
+ // Trigger stays expanded until the hide animation ends
353
+ await (0, test_1.waitFor)(() => {
354
+ (0, test_1.expect)(trigger).toHaveAttribute("aria-expanded", "false");
355
+ });
349
356
  (0, test_1.expect)(args.handleChange).toHaveBeenCalledTimes(1);
350
357
  (0, test_1.expect)(args.handleChange).toHaveBeenCalledWith({
351
358
  name: "animal",
@@ -616,6 +623,10 @@ exports.defaultHandlers = {
616
623
  });
617
624
  // Controlled
618
625
  await test_1.userEvent.click(document.body);
626
+ // Wait for the previous menu to finish its hide animation
627
+ await (0, test_1.waitFor)(() => {
628
+ (0, test_1.expect)((0, test_1.within)(canvasElement.ownerDocument.body).queryAllByRole("option")).toHaveLength(0);
629
+ });
619
630
  const [__, controlled, focusable] = canvas.getAllByRole("button");
620
631
  const hiddenInputs2 = canvasElement.querySelectorAll('input[type="hidden"]');
621
632
  const [___, inputControlled] = Array.from(hiddenInputs2);
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import type * as T from "./Freeze.types";
3
+ declare const Freeze: React.FC<T.Props>;
4
+ export default Freeze;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import s from "./Freeze.module.css";
5
+ /**
6
+ * Transitions never commit an already visible Suspense boundary switching to its fallback,
7
+ * so we stop freezing if the frozen render isn't committed in time instead of blocking them forever
8
+ */
9
+ const COMMIT_TIMEOUT = 100;
10
+ const infinitePromise = new Promise(() => { });
11
+ const Suspend = (props) => {
12
+ const state = props.stateRef.current;
13
+ if (state.expired)
14
+ return null;
15
+ if (state.committed)
16
+ throw infinitePromise;
17
+ state.pending ??= new Promise((resolve) => {
18
+ setTimeout(() => {
19
+ state.expired = !state.committed;
20
+ resolve();
21
+ }, COMMIT_TIMEOUT);
22
+ });
23
+ throw state.pending;
24
+ };
25
+ const Freeze = (props) => {
26
+ const { frozen, children } = props;
27
+ const stateRef = React.useRef({});
28
+ const elRef = React.useRef(null);
29
+ // Insertion effect is the earliest opportunity to undo the Suspense display: none
30
+ React.useInsertionEffect(() => {
31
+ if (!frozen) {
32
+ stateRef.current = {};
33
+ return;
34
+ }
35
+ if (stateRef.current.expired)
36
+ return;
37
+ stateRef.current.committed = true;
38
+ elRef.current?.style.removeProperty("display");
39
+ }, [frozen]);
40
+ return (_jsx(React.Suspense, { children: _jsxs("div", { className: s.root, ref: (el) => {
41
+ if (el)
42
+ elRef.current = el;
43
+ }, children: [frozen && _jsx(Suspend, { stateRef: stateRef }), children] }) }));
44
+ };
45
+ Freeze.displayName = "Freeze";
46
+ export default Freeze;
@@ -0,0 +1 @@
1
+ .root{display:contents}