Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shadow-Dom Support #402

Open
hallzy opened this issue Oct 19, 2021 · 13 comments
Open

Shadow-Dom Support #402

hallzy opened this issue Oct 19, 2021 · 13 comments

Comments

@hallzy
Copy link

hallzy commented Oct 19, 2021

Is your feature request related to a problem? Please describe.

We are developing something that uses a shadow dom, but it seems like this project doesn't work fully inside of a shadow dom.

If I understand the code correctly, chrome on desktop shouldn't have a 30px border-right and right on 2 of the os content wrappers, but that is what we see inside of the shadow dom, so at the very least it seems like the detection that exists to determine if those styles should be applied doesn't work correctly inside a shadow.

This basically means that without adding our own code, we end up with different gaps between the content of the scrollable area and the scrollbar for different browsers.

What we have ended up doing in the meantime is calculating the width of the scrollbar on our own, and adding a margin to our content based on that calculation, which works, but having it work built in would be more convenient.

I don't know if there are other things not working as intended, this is just the one that we've noticed.

Also, to make it more convenient to use, it would be nice to be able to easily import the JS as a module. We have a workaround for that right now as well.

Describe the solution you'd like

Support for use inside of a shadow dom, and a JS file that can be imported as a module.

Describe alternatives you've considered

As mentioned, We currently determine the width of the scrollbar ourselves and add a CSS style to account for it.

For importing the JS, we are adding the script tag for that into an iframe, and a promise gives us back the OverlayScrollbars function (a frame is used because our product is embedded onto customer sites, and we are trying to not pollute their global scope).

@KingSora
Copy link
Owner

Good day @hallzy :)
Thanks for the detailed description! Indeed I've never tried to use the library in Shadow DOM yet, so I'm not sure how it reacts... Is it possible for you to create a small example on JSFiddle, CodeSandbox or StackBlitz?
Can you also provide me the output of the command OverlayScrollbars.globals() and the name of your operating system?

@hallzy
Copy link
Author

hallzy commented Oct 22, 2021

I don't know about "small", but I have created as small of an example as I can. https://jsfiddle.net/ex4p6mja/105/

While creating this, I realized that it isn't actually an issue with being inside a shadow dom, so much as it is with us trying to not pollute the global scope.

If I include OverlayScrollbars as a script tag in the main HTML document (or even inside the shadow dom) it works fine. But both of those solutions mean that we have an OverlayScrollbars function attached to the main window object (shadow doms do not have their own window object).

So, what we do is use ES modules to import code that we want into the files that we need access to that code. In some cases, as is the case with OverlayScrollbars, there is no module we can import like this. So our workaround is to create a throwaway iframe that we load all of our scripts in that cannot be loaded as a module, and when the script is finished loading we resolve a promise which we can import which has the result of the script execution.

There isn't really a way to import modules that I could find in JS fiddle, so I just put it into the same file, but it would normally be imported.

Once the promise finishes we call the scrollbar function on the scrollable div I created

And the getHTML and getCSS functions at the bottom can be thought of as template imports that we would normally have so that we can easily insert this stuff into the shadow.

So basically, we can work with OverlayScrollbars even if we don't have a module, but if we could just import a module and have that work, that would be easiest.

And at least with the way we are doing it now with the iframe, we end up with a 30px right and border in chrome which as I mentioned in my previous description, I don't think it supposed to happen.

I hope this helps :D

My OS is Ubuntu, and the output of OverlayScrollbars.globals() is:

{
    "defaultOptions": {
        "className": "os-theme-dark",
        "resize": "none",
        "sizeAutoCapable": true,
        "clipAlways": true,
        "normalizeRTL": true,
        "paddingAbsolute": false,
        "autoUpdate": null,
        "autoUpdateInterval": 33,
        "updateOnLoad": [
            "img"
        ],
        "nativeScrollbarsOverlaid": {
            "showNativeScrollbars": false,
            "initialize": true
        },
        "overflowBehavior": {
            "x": "scroll",
            "y": "scroll"
        },
        "scrollbars": {
            "visibility": "auto",
            "autoHide": "never",
            "autoHideDelay": 800,
            "dragScrolling": true,
            "clickScrolling": false,
            "touchSupport": true,
            "snapHandle": false
        },
        "textarea": {
            "dynWidth": false,
            "dynHeight": false,
            "inheritedAttrs": [
                "style",
                "class"
            ]
        },
        "callbacks": {
            "onInitialized": null,
            "onInitializationWithdrawn": null,
            "onDestroyed": null,
            "onScrollStart": null,
            "onScroll": null,
            "onScrollStop": null,
            "onOverflowChanged": null,
            "onOverflowAmountChanged": null,
            "onDirectionChanged": null,
            "onContentSizeChanged": null,
            "onHostSizeChanged": null,
            "onUpdated": null
        }
    },
    "autoUpdateLoop": false,
    "autoUpdateRecommended": false,
    "nativeScrollbarSize": {
        "x": 0,
        "y": 0
    },
    "nativeScrollbarIsOverlaid": {
        "x": true,
        "y": true
    },
    "nativeScrollbarStyling": false,
    "overlayScrollbarDummySize": {
        "x": 30,
        "y": 30
    },
    "cssCalc": "calc",
    "restrictedMeasuring": false,
    "rtlScrollBehavior": {
        "i": true,
        "n": false
    },
    "supportTransform": true,
    "supportTransition": true,
    "supportPassiveEvents": true,
    "supportResizeObserver": true,
    "supportMutationObserver": true
}

@hallzy
Copy link
Author

hallzy commented Oct 27, 2021

Hi @KingSora, just wondering if you've had a chance to look at this yet :)

@KingSora
Copy link
Owner

@hallzy not yet.. sorry I'm a bit busy, but I'll try to do it this weekend :)

@hallzy
Copy link
Author

hallzy commented Oct 27, 2021

No problemo 😃

@lincolnthree
Copy link

Has anyone gotten this working with a viewport / scroll container contained within a shadow DOM element / component?

@lincolnthree
Copy link

lincolnthree commented Oct 14, 2024

Adding my notes to this. I was able to get OverlayScrollbars working in the Shadow DOM, but it required custom initialization (see docs: https://kingsora.github.io/OverlayScrollbars/), and also required injecting the OverlayScrollbars styles into the shadow DOM element's root <style> element.

See this issue for notes: #682 (comment)

You also need to manually call instance.update() periodically on the OverlayScrollbars object you instantiate in order to get it to properly init or resize whenever content is added or removed to the Shadow DOM elements, if that comes in dynamically (if that content would change the height of the scrolling content/element).

@NathyVZM
Copy link

Adding my notes to this. I was able to get OverlayScrollbars working in the Shadow DOM, but it required custom initialization (see docs: https://kingsora.github.io/OverlayScrollbars/), and also required injecting the OverlayScrollbars styles into the shadow DOM element's root <style> element.

See this issue for notes: #682 (comment)

You also need to manually call instance.update() periodically on the OverlayScrollbars object you instantiate in order to get it to properly init or resize whenever content is added or removed to the Shadow DOM elements, if that comes in dynamically (if that content would change the height of the scrolling content/element).

Hii theree, hope you're well 💗 I'm currently experiencing this issue of the Shadow DOM, I already injected the styles to the elements Shadow DOM using adoptedStyleSheets.push but the scrollbar has an odd behavior in Chrome, Opera and Edge, in Firefox works well. Basically the track doesn't move smoothly, it either goes to the end or the start, not sure if it can be fixed with the instance.update() you're commenting. Could you let me know how did you fix your issue? Where should cI call this update function?

@cywcd
Copy link

cywcd commented Dec 10, 2024

Adding my notes to this. I was able to get OverlayScrollbars working in the Shadow DOM, but it required custom initialization (see docs: https://kingsora.github.io/OverlayScrollbars/), and also required injecting the OverlayScrollbars styles into the shadow DOM element's root <style> element.
See this issue for notes: #682 (comment)
You also need to manually call instance.update() periodically on the OverlayScrollbars object you instantiate in order to get it to properly init or resize whenever content is added or removed to the Shadow DOM elements, if that comes in dynamically (if that content would change the height of the scrolling content/element).

Hii theree, hope you're well 💗 I'm currently experiencing this issue of the Shadow DOM, I already injected the styles to the elements Shadow DOM using adoptedStyleSheets.push but the scrollbar has an odd behavior in Chrome, Opera and Edge, in Firefox works well. Basically the track doesn't move smoothly, it either goes to the end or the start, not sure if it can be fixed with the instance.update() you're commenting. Could you let me know how did you fix your issue? Where should cI call this update function?

Hello, I have the same problem, is there a solution?
I am developing a webcomponent based on lit, initializing OverlayScrollbars in shadowDom, and calling the instance's update method does not solve the problem. The scrollbar cannot be moved smoothly, either shaking at the beginning or jumping to the end.

@hallzy
Copy link
Author

hallzy commented Dec 10, 2024

So, for some reason I never went back to this to explain what I ended up doing. Unfortunately, it was for work, and I was laid off 6 months ago and no longer have access to the code and I cannot remember what we ended up doing.

we did use this scroll bar though.

sorry

@NathyVZM
Copy link

NathyVZM commented Dec 10, 2024

Hello, I have the same problem, is there a solution?
I am developing a webcomponent based on lit, initializing OverlayScrollbars in shadowDom, and calling the instance's update method does not solve the problem. The scrollbar cannot be moved smoothly, either shaking at the beginning or jumping to the end.

hii there! we finally managed to make it work! we're also developing a web component using Lit and aaalso using Material Design, our component uses md-menu for dropdowns and stuff, here's the code:

/**
     * Attaches a scroll event listener to the dropdown element.
     * The listener triggers the `_handleScroll` method when the user scrolls within the dropdown.
     *
     * @private
     */
    private _attachScrollListener() {
        const scrollbarStylesheet = new CSSStyleSheet();
        scrollbarStylesheet.replaceSync(overlayscrollbarStyles);

        scrollbarStylesheet.insertRule(`
            .os-theme-dark {
                --os-handle-bg: var(--cds-menu-scrollbar-thumb-color, var(--cds-primary-color, #849BC6FF)) !important;
                --os-handle-bg-hover: var(--cds-menu-scrollbar-thumb-hover-color, var(--cds-menu-scrollbar-thumb-color, var(--cds-primary-color, #849BC6FF))) !important;
                --os-handle-bg-active: var(--cds-menu-scrollbar-thumb-active-color, var(--cds-menu-scrollbar-thumb-color, var(--cds-primary-color, #849BC6FF))) !important;
            }
        `);

        this._dropdownRef.value?.shadowRoot?.adoptedStyleSheets.push(scrollbarStylesheet);
        const items = this._dropdownRef.value?.shadowRoot?.querySelector('.items') as HTMLDivElement;

        const scrollbar = OverlayScrollbars(items, {
            scrollbars: { autoHide: 'scroll' },
            overflow: { x: 'hidden' },
        }, {
            scroll: (_, ev) => {
                const scrollContainer = ev.target as HTMLDivElement;

                if (!scrollContainer) {
                    return;
                }

                const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
                const scrollPercentage = scrollTop / (scrollHeight - clientHeight);
                const scrollbarElement = scrollbar.elements().scrollbarVertical.scrollbar;

                scrollbarElement.style.setProperty('--os-scroll-direction', `${scrollPercentage > 0.5 ? 1 - scrollPercentage : scrollPercentage}`);
                scrollbarElement.style.setProperty('--os-scroll-percent', `${scrollPercentage}`);

                this._handleScroll(ev);
            }
        });

        const viewport = scrollbar.elements().viewport;
        viewport.style.setProperty('overflow', `var(${this._menuOverflowCssVar}, hidden scroll)`, 'important');

        setInterval(() => scrollbar.update(), 250);
    }

call this function in connectedCallback and firstUpdated, might work only with the latter but just in case, test both. .items would be the element to which you need to attach the scrollbar, so change the code to fit your needs 🫡 we basically need to inject the scrollbar styles into the shadow dom of the element, do the calculation of the --os-scroll-direction and --os-scroll-percent that don't work in the shadow dom, aaand do that update of the scroll every 250ms, you can change this number and try on your end

cc: @hallzy

@aereaco
Copy link

aereaco commented Jan 17, 2025

Global OverlayScrollbars

Works for both Initial & Dynamically Loaded Content as well as Web Components

Some background:

I had some JS code to implement the requested feature in Tailwind based projects specifically. Tailwind's inline CSS approach has you define your elements' look and behavior with classes. I adapted the code to not only look for all elements with Tailwind's overlay scroll or auto classes but also the [data-overlayscrollbars-initialize] attribute. However, you can change it to look for what ever classes or attributes you want. Have a look at the code below.

The following code works with classes or attributes and is style system agnostic

var { OverlayScrollbars, ScrollbarsHidingPlugin, SizeObserverPlugin, ClickScrollPlugin } = OverlayScrollbarsGlobal;

OverlayScrollbars.plugin([ScrollbarsHidingPlugin, SizeObserverPlugin, ClickScrollPlugin]);

const scrollableClasses = [ // Your Tailwind classes (or any other classes)
    '.overflow-auto', '.overflow-y-auto', '.overflow-x-auto',
    '.overflow-scroll', '.overflow-y-scroll', '.overflow-x-scroll'
];

const scrollableAttributes = [ // Attributes to trigger initialization
    '[data-overlayscrollbars-initialize]'
];

const osOptions = {
    scrollbars: {
        autoHide: 'move',
        autoHideDelay: 800,
        dragScrolling: true,
        clickScrolling: true,
        touchSupport: true,
        snapHandle: false,
    },
};

function initializeOverlayScrollbars(element) {
    if (!element) return;

    let shouldInitialize = false;

    // Check for classes
    if (element.classList) {
        if (scrollableClasses.some(cls => element.classList.contains(cls.substring(1)))) {
            shouldInitialize = true;
        }
    }

    // Check for attributes
    if (!shouldInitialize) { // Only check attributes if classes didn't match
        if (scrollableAttributes.some(attr => element.matches(attr))) {
            shouldInitialize = true;
        }
    }

    if (shouldInitialize) {
        try {
            if (!element.OverlayScrollbarsInitialized) {
                requestAnimationFrame(() => {
                    element.osInstance = OverlayScrollbars(element, osOptions);
                    element.OverlayScrollbarsInitialized = true;
                    console.log('OverlayScrollbars initialized on:', element);
                });
            }
        } catch (error) {
            console.error('OverlayScrollbars initialization failed:', element, error);
        }
    } else if (element.OverlayScrollbarsInitialized) {
        destroyOverlayScrollbars(element);
    }
}

function destroyOverlayScrollbars(element) {
    if (element.OverlayScrollbarsInitialized) {
        if (element.osInstance) {
            element.osInstance.destroy();
            delete element.osInstance;
        }
        delete element.OverlayScrollbarsInitialized;
        console.log('OverlayScrollbars destroyed on:', element);
    }
}

document.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll([...scrollableClasses, ...scrollableAttributes].join(',')).forEach(initializeOverlayScrollbars);
});

const observer = new MutationObserver(mutations => {
    mutations.forEach(mutation => {
        if (mutation.type === 'childList') {
            mutation.addedNodes.forEach(node => {
                if (node instanceof Element) {
                    initializeOverlayScrollbars(node);
                    node.querySelectorAll([...scrollableClasses, ...scrollableAttributes].join(',')).forEach(initializeOverlayScrollbars);
                }
            });
            mutation.removedNodes.forEach(node => {
                if (node instanceof Element) {
                    destroyOverlayScrollbars(node);
                    node.querySelectorAll([...scrollableClasses, ...scrollableAttributes].join(',')).forEach(destroyOverlayScrollbars);
                }
            })
        } else if (mutation.type === 'attributes' && scrollableAttributes.includes(`[${mutation.attributeName}]`)) {
            initializeOverlayScrollbars(mutation.target);
        } else if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
          initializeOverlayScrollbars(mutation.target);
        }
    });
});

observer.observe(document.body, { childList: true, subtree: true, attributes: true });

Note regarding a ComputedStyle implementation

I tried to implement a similar approach based on the ComputedStyle of all elements and while it worked well for static content it didn't work for dynamically added content. When I revised for dynamically added content it had severe performance issues and rendered the page and browser useless. But just for reference here is the code that works for static pages

var { 
    OverlayScrollbars, 
    ScrollbarsHidingPlugin, 
    SizeObserverPlugin, 
    ClickScrollPlugin
} = OverlayScrollbarsGlobal;

// Target the body and all scrollable elements based on overflow property
var elements = document.querySelectorAll('body, *');

elements.forEach(function(element) {
    const style = window.getComputedStyle(element);
    const isScrollable = style.overflow === 'auto' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflowY === 'scroll' || style.overflowX === 'auto' || style.overflowX === 'scroll';

    // Apply OverlayScrollbars with autoHide option if the element is scrollable
    if (isScrollable || element === document.body) {
        OverlayScrollbars(element, {
            scrollbars: {
                autoHide: "move",
                autoHideDelay: 800,
                dragScrolling: true,
                clickScrolling: true,
                touchSupport: true,
                snapHandle: false
            }
        });
    }
});

Hope some will find this information useful

@MaksimShakavin
Copy link

do the calculation of the --os-scroll-direction and --os-scroll-percent that don't work in the shadow dom, aaand do that update of the scroll every 250ms, you can change this number and try on your end

I investigated why the --os-scroll-percent variable is not updated correctly in Shadow DOM, and it seems to be a Blink bug. The issue with the jumping scrollbar handle is only reproducible in Chrome and Edge, which are the only browsers where the ScrollTimeline API is available.

The library uses the .animate() API to update the value of the --os-scroll-percent variable. However, inside a Shadow DOM context, the animation function only interpolates discrete values (0 and 1) for this variable, even when animation.currentTime is a correct floating-point number.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants