adobe / react-spectrum

A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
https://react-spectrum.adobe.com
Apache License 2.0
12.58k stars 1.09k forks source link

allow subscribing to `usePreventScroll` state #4782

Open Talor-A opened 1 year ago

Talor-A commented 1 year ago

πŸ™‹ Feature Request

we have a header component with position:fixed overlaid on the main body content. when a modal, menu, or popover opens, padding is added to the <html> element to compensate for the removed scrollbar.

however, no padding is added to our fixed position <header>, so the content inside jumps a number of px equal to the size of the scrollbar. this means that menu triggers in the header move around, so they become much harder to toggle!

it would be great to have a way to know that scrolling has been locked, so that we could apply custom styles to any elements that need it besides the body.

πŸ€” Expected Behavior

make the state of usePreventScroll consumable from outside the context of the components that call the hook

😯 Current Behavior

there's no way to know whether scroll is being prevented. it's applied via inline styles onto the html element, and the state is stored in a locally scoped variable preventScrollCount

πŸ’ Possible Solution

there are many ways to solve for this:

  1. apply a data-scroll-prevented attribute to the html element when locking scroll. this would allow using css selectors, event listeners, or observers to subscribe to the state.
  2. apply a custom class to the html element.
  3. expose a useIsScrollPrevented hook that allows subscribing to state in js-land. this would probably mean changing preventScrollCount to some kind of basic observable value / EventTarget, since it would be undesirable to use React.Context. I don't see an existing global state management solution in react-aria, but anything would work.

happy to help ship a feature if we can decide on the best path forward!

πŸ”¦ Context

πŸ’» Examples

examples implementing the three suggestions above, respectively:


const Header = () => (
  <>
    <header className="site-header">{...}</header>
    <style>`
      html[data-scroll-prevented=true] header.site-header {
         marginRight: ${window.innerWidth - document.documentElement.clientWidth};
      }
      `
    </style>
  </>
)

const Header = () => (
  <>
    <header className="site-header">{...}</header>
    <style>`
      html.scroll-prevented header.site-header {
         marginRight: ${window.innerWidth - document.documentElement.clientWidth};
      }
      `
    </style>
  </>
)

const Header = () => {
  const isScrollPrevented = useIsScrollPrevented();

  return  (<>
    <header className={isScrollPrevented ? "site-header scroll-prevented" : "site-header"}>{...}</header>
    <style>`
      header.site-header.scroll-prevented {
         marginRight: ${window.innerWidth - document.documentElement.clientWidth};
      }
      `
    </style>
  </>)
}

🧒 Your Company/Team

https://replit.com

🎁 Tracking Ticket (optional)

(private: https://linear.app/replit/issue/I2C-125)

LFDanLu commented 1 year ago

I think applying a data attribute to the html element would be reasonable, we actually do the following to check if scrolling is prevented ourselves: https://github.com/adobe/react-spectrum/blob/b57b3af44c345d456658e2895bb9244c65f0e7d3/packages/%40react-aria/utils/src/scrollIntoView.ts#L87

Talor-A commented 1 year ago

we actually do the following to check if scrolling is prevented ourselves

thanks. that got me thinking, subscribing to this value using a mutation observer is a reasonable workaround for the time being!

var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutationRecord) {
        console.log('style changed!');
    });    
});

var target = document.querySelector('html')
observer.observe(target, { attributes : true, attributeFilter : ['style'] });