50

I've got your typical dropdown navigation, and I'm trying to make sure the drop menu links are always accessible and visible:

<li><a href="#">Link 1</a>
    <ul>
        <li><a href="#">Link 1</a></li>
        <li><a href="#">Link 2</a></li>
        <li><a href="#">Link 3</a></li>
    </ul>
</li>
<li><a href="#">Link 2</a>
    <ul>
        <li><a href="#">Link 1</a></li>
        <li><a href="#">Link 2</a></li>
        <li><a href="#">Link 3</a></li>
    </ul>
</li>
<!-- etc. -->
</ul>

The CSS really isn't anything special (colors and backgrounds removed):

.dropdown,
.dropdown li,
.dropdown ul {
    list-style:none;
    margin:0;
    padding:0;
}
.dropdown {
    position:relative;
    z-index:10000;
    float:left;
    width:100%;
}
.dropdown ul {
    position:absolute;
    top:100%;
    visibility:hidden;
    display:none;
    width:16em;
}
.dropdown ul ul {
    top:0;
    left:100%;
}
.dropdown li {
    position:relative;
    float:left;
}
.dropdown li:hover{
    z-index:910;
}
.dropdown ul:hover,
.dropdown li:hover > ul,
.dropdown a:hover + ul,
.dropdown a:focus + ul {
    visibility:visible;
    display:block;
}
.dropdown a {
    display:block;
    padding:1em 2em;
}
.dropdown ul li {
    width:100%;
}

There are an unknown number of top level links (they are created by the user). The problem I'm having is that sometimes the drop menus (which go to the right) will go off screen if the top level link is too far to the right. I added this bit of CSS to compensate:

.dropdown > li:last-child ul { /* ...or use a class on the last link for IE */
    right:0;
}

Now the last one goes to the left instead of off screen, which is nice, but there are a few issues:

  1. I don't always need these styles for the last link since it isn't always at the edge of the screen (like if there are only 3 links).
  2. When the browser window is resized the links stack on top of each other (by design). Sometimes links in the middle of the sequence end up on the right edge, and their dropdowns are cut off.
  3. Sometimes the "next to last" link's menu will go outside the boundary as well.

Resize the panel in this demo to see what I mean (the red area is considered "off screen") http://jsfiddle.net/G7qfq/

I've struggled with this annoyingly common problem for years and have never found a satisfactory solution. Is there any way to check if the drop menu would go off screen, and if so, add/remove a class name or something so I can keep it on screen with CSS?

One clue I might use is that if a menu does go off screen, it always produces a vertical scroll bar at the bottom of the window, but I'm not sure how to use that knowledge. I tried the accepted answer to this question about detecting vertical scroll bars, but for some reason it always returns true, and always adds the "edge" class (maybe there's an issue with the timing?):

$(".dropdown li").on('mouseenter mouseleave', function (e) {

    // Get the computed style of the body element
    var cStyle = document.body.currentStyle||window.getComputedStyle(document.body, "");

    // Check the overflow and overflowY properties for "auto" and "visible" values
    hasVScroll = cStyle.overflow == "visible" 
             || cStyle.overflowY == "visible"
             || (hasVScroll && cStyle.overflow == "auto")
             || (hasVScroll && cStyle.overflowY == "auto");

    if (hasVScroll) {
        $(this).addClass('edge');
    } else {
        $(this).removeClass('edge');
    }
});​

Demo with the javascript: http://jsfiddle.net/G7qfq/2/

Really, I don't want to see a vertical scroll bar even for a split second so I'm not sure that's the way to go, plus there could be false positives (scroll bar for some other reason).

I also tried the solution in this answer which I admit, I don't quite understand, and couldn't get it to work: http://jsfiddle.net/G7qfq/3/

$(".dropdown li").on('mouseenter mouseleave', function (e) {

    var elm = $('ul:first', this);
    var off = elm .offset();
    var t = off.top;
    var l = off.left;
    var h = elm.height();
    var w = elm.width();
    var docH = $(window).height();
    var docW = $(window).width();

    var isEntirelyVisible = (t > 0 && l > 0 && t + h < docH && l+ w < docW);

    if ( ! isEntirelyVisible ) {
        $(this).addClass('edge');
    } else {
        $(this).removeClass('edge');
    }
});​

I assume the solution requires javascript, and I am using jQuery, but I haven't got a clue how to approach the problem. Any ideas?

Community
  • 1
  • 1
Wesley Murch
  • 101,186
  • 37
  • 194
  • 228
  • http://stackoverflow.com/questions/1725508/how-can-i-determine-if-an-html-element-is-offscreen this might be helpful. – Arkadiusz 'flies' Rzadkowolski Jul 16 '12 at 20:31
  • I'll take a look at that and update my results, thanks. On second thought, the "scrollbar detect" method isn't very good and might result in false positives if it worked (scroll bar for another reason). – Wesley Murch Jul 16 '12 at 20:36
  • Something like this http://jsfiddle.net/j08691/SZMY5/1/embedded/result/ ? – j08691 Jul 16 '12 at 20:45
  • Yes, something very much like that, except on the top level dropdowns, and with stackable nav items (if the viewport is small). I'll see if I can pick that code apart and find something useful, thanks for understanding my problem. – Wesley Murch Jul 16 '12 at 20:46
  • @WesleyMurch - It's kind of old. Wrote it a while ago and ended up not using it. I'm sure there's plenty of room for updates and improvements. Hope it helps. – j08691 Jul 16 '12 at 21:10
  • It is better to define width for the anchor tag used in your menu. It worked for me. – Arun Jain Jul 17 '12 at 11:54
  • Not an answer, per se, but just to caveat the `isEntirelyVisible` piece, there is a good plugin to jQuery called [isOnScreen](https://github.com/moagrius/isOnScreen) which can be used in differing ways to determine if part or all of the popup/dropdown (or any element) is entirely visible within the viewport. – Jasel Jul 10 '17 at 17:00

3 Answers3

64

I think you were almost there...

You should really only be interested in the calculations involved in the width. If the width of the dropdown element and the offset of that element is greater than the width of the container, you want to switch your menu.

$(function () {
    $(".dropdown li").on('mouseenter mouseleave', function (e) {
        if ($('ul', this).length) {
            var elm = $('ul:first', this);
            var off = elm.offset();
            var l = off.left;
            var w = elm.width();
            var docH = $(".container").height();
            var docW = $(".container").width();

            var isEntirelyVisible = (l + w <= docW);

            if (!isEntirelyVisible) {
                $(this).addClass('edge');
            } else {
                $(this).removeClass('edge');
            }
        }
    });
});

http://jsfiddle.net/G7qfq/582/

r0m4n
  • 3,474
  • 3
  • 34
  • 43
  • 3
    You nailed it! Most of my ideas didn't come until I actually posted the question (of course), my brain was so fried from working on it I didn't realize my error. You have my sincere gratitude, this code's going in the tool box for sure. – Wesley Murch Jul 17 '12 at 16:43
  • This seems to be giving an error "Uncaught TypeError: Cannot read property 'left' of undefined" even in your jsfiddle. Anyone know how to fix this? – Aaron Jul 17 '15 at 13:15
  • @Aaron Ah yea, nice catch... see update. The errors are caused by the handler attempting to reposition child nested items when they don't exist! – r0m4n Jul 20 '15 at 05:15
  • Hi @r0m4n Thank you for excellent answer. But I have minor issue that I want to use for 2nd level but it doesn't work. – The Hung Jul 27 '15 at 06:53
  • @r0m4n would this be able to work with a multi level drop down? I have a drop down that goes 2 levels deep and need to be able to offset the second list from the first list. – pertrai1 Aug 01 '15 at 18:20
  • nice, but you never used `docH` – Muhammad Omer Aslam Jul 26 '19 at 23:42
  • can i have use for RTL? will need any changes for RTL? – HDP Dec 13 '21 at 09:28
  • Why are we not also concerned with the height? If near the top or bottom of the viewport, the dropdown or tooltip must be moved. – David Spector Aug 16 '23 at 15:58
2

Here is a function that can be used for menus that fly-out to the right, or down (based off @r0m4n's code):

function fixFlyout (containerElement, parentElement,flyoutElement,flyoutDirection) {
    $(parentElement).on('mouseenter mouseleave', function (e) {
        var element = $(flyoutElement, this);
        var offset = element .offset();
        switch(flyoutDirection) {
            case 'down':
                var top = offset.top;
                var height = element.height();
                var windowHeight = $(containerElement).height();
                var isEntirelyVisible = (top + height <= windowHeight);
                break;
            case 'right':
                var left = offset.left;
                var width = element.width();
                var windowWidth = $(containerElement).width();
                var isEntirelyVisible = (left + width <= windowWidth);
                break;
        }
        if (!isEntirelyVisible ) {
            $(element).addClass('edge');
        } else {
            $(element).removeClass('edge');
        }
    });
}
//Level 1 Flyout
fixFlyout(containerElement = '.header',parentElement = '.header .navigation>.menu>.expanded',flyoutElement = '.menu:first',flyoutDirection = 'down');
yaakov
  • 4,568
  • 4
  • 27
  • 51
Adrian
  • 171
  • 2
  • Would you be able to add a fiddle that shows this being used? I want to be able to have nested lists inside of nested lists for multi-level dropdowns that go to the right or to the left – pertrai1 Aug 01 '15 at 18:18
  • looks like a typo in case 'right' var isEntirelyVisible should be left + width, yes? –  Feb 05 '16 at 18:31
0

I was not able to get addClass('edge') to work correctly but I was able to modify the CSS for the element in question to achieve the behavior. (only slightly modified from r0m4n):

        //Align submenu to right if cut-off from window
        $(function () {
            $(".dropdown").on('mouseenter mouseleave', function (e) {
                if ($('.dropdown-content', this).length) {
                    var dropdownElement = $('.dropdown-content:first', this);
                    var elementOffset = dropdownElement.offset();
                    var elementOffsetLeft = elementOffset.left;
                    var elementWidth = dropdownElement.width();
                    var pageHeigth = $(".show-on-scroll-wrapper").height();
                    var pageWidth = $(".show-on-scroll-wrapper").width();
                    //if left offset + width of dropdown is bigger than container width then it is cut-off
                    if ((elementOffsetLeft + elementWidth) > pageWidth) {
                        //Align Right
                        $(".dropdown-content").css({ "left":"auto", "right":"0", "margin-right":"-10px;"});
                    } else {
                        //Align Left
                        $(".dropdown-content").css({ "left":"0", "right":"auto", "margin-left": "-10px;" });
                    }
                }
            });
        });
RBILLC
  • 170
  • 2
  • 6