vrintle
vrintle

Reputation: 5586

Fixed height parent should include absolute / fixed positioned child

I am referring div.settings as the "parent" and #mainMenu as the "child".

Now, I want to create a dropdown similar to one in Windows. So, the height of the parent needs to be fixed (as it was initially). Also, I want my child to be fixed positioned to be able to create the below effect (I will create the effect myself later on).

enter image description here


This is what I have done so far.

* {
  box-sizing: border-box;
  cursor: default;
  user-select: none;
  font-family: Roboto;
  font-weight: 400;
}

.settings {
  padding: 7px 16px;
  border: 0.5px solid #7777;
  border-radius: 4px;
}

#mainMenu {
  position: fixed;
  /* or absolute */
  display: inline-flex;
  flex-flow: column wrap;
  justify-content: flex-start;
  align-items: stretch;
}

span.opt {
  padding: 5px 20px;
  flex-grow: 1;
  margin: 0;
  background: #777;
  color: #FFF;
  text-align: center;
  display: none;
}

span.selected {
  display: inline-block !important;
  background: #28F;
}

#mainMenu:hover>span.opt {
  display: inline-block;
}
<div class='settings'>
  <p>Select default browser</p>
  <span id='mainMenu'>
    <span class='opt'>Google Chrome</span>
    <span class='opt'>Mozilla Firefox</span>
    <span class='opt'>Safari</span>
    <span class='opt selected'>Microsoft Edge</span>
    <span class='opt'>Opera</span>
    <span class='opt'>Internet Explorer</span>
  </span>
</div>

Problem

The fixed positioned child is overflowing from the body of its parent.

I want the parent to include the menu inside its content. Something like in the below picture:

enter image description here

The red line (roughly) indicates that where the border of the parent should be.

Note: Only CSS (because nothing is dynamic without the effect), and I have displayed only one of the menu in my code (to minimise complexity and code length). In reality, there could be many.

In short, I just want the child to remain fixed-positioned and inside the body of the parent with fixed height.

Upvotes: 1

Views: 204

Answers (4)

Arleigh Hix
Arleigh Hix

Reputation: 10897

You can wrap your menus in a div with a min-height to hold that space in the parent div, or use a "placeholder" span.opt.selected with visibility: hidden (see updated snippet).

* {
  box-sizing: border-box;
  cursor: default;
  user-select: none;
  font-family: Roboto;
  font-weight: 400;
}

.settings {
  padding: 7px 16px;
  border: 0.5px solid #7777;
  border-radius: 4px;
  background-color: inherit;
}

#mainMenu {
  position: fixed;
  /* or absolute */
  display: inline-flex;
  flex-flow: column wrap;
  justify-content: flex-start;
  align-items: stretch;
}

span.opt {
  padding: 5px 12px;
  flex-grow: 1;
  margin: 0;
  background: #777;
  color: #FFF;
  text-align: center;
  display: none;
}

span.placeholder {
  visibility: hidden;
}

span.selected {
  display: inline-block !important;
  background: #28F;
}

#mainMenu:hover>span.opt {
  display: inline-block;
}
<div class='settings'>
  <p>Select default browser</p>
  <span id='mainMenu'>
    <span class='opt'>Google Chrome</span>
    <span class='opt'>Mozilla Firefox</span>
    <span class='opt'>Safari</span>
    <span class='opt selected'>Microsoft Edge</span>
    <span class='opt'>Opera</span>
    <span class='opt'>Internet Explorer</span>
  </span>
  <span class='opt selected placeholder'>placeholder</span>
</div>

Upvotes: 1

user3579536
user3579536

Reputation:

JavaScript and CSS Solution Proposal

In order to take controls on positioning should be good to keep the options and the selected option in two different contexts and then operate on the top property of the option programmatically. After that, you have to tackle the dynamic changes of the top property and the value getting and setting in the markup.

function setTop() {
  let height = document.querySelector(".opt").clientHeight;
  document.querySelector(".options").style.top = height * 3 * (-1) + "px";
}
setTop();
* {
      box-sizing: border-box;
      cursor: default;
      user-select: none;
      font-family: Roboto;
      font-weight: 400;
    }

    .settings {
      padding: 7px 16px;
      border: 0.5px solid #7777;
      border-radius: 4px;
    }

    #mainMenu {
      position: relative;
      display: block;
      width:300px;
    }
    #mainMenu .options {
      position: absolute;
      display: none;
      width:100%;
    }
    #mainMenu:hover .options {
      display: block;
    }

    #mainMenu .opt {
      padding: 5px 20px;
      background: #777;
      color: #FFF;
      text-align: center;
    }
    #mainMenu .opt.selected {
      background: #28F;
    }
    #mainMenu .selection {
      display:block;
    }
<div class='settings'>
      <p>Select default browser</p>
      <div id='mainMenu'>
        <span class='opt selected selection'>Microsoft Edge</span>
        <div class='options' style="top:0;">
          <div class='opt'>Google Chrome</div>
          <div class='opt'>Mozilla Firefox</div>
          <div class='opt'>Safari</div>
          <div class='opt selected'>Microsoft Edge</div>
          <div class='opt'>Opera</div>
          <div class='opt'>Internet Explorer</div>
        </div>
      </div>
    </div>

Upvotes: 1

sol
sol

Reputation: 22949

I want the parent to include the menu inside its content

Adding position absolute or fixed to the menu takes it out of the content flow, so you won't get the layout you want without restructuring the HTML a little.

Demo added below, let me know if you have any questions. You will need to add additional logic to check if menu is out of viewport bounds.

const MENU = document.getElementById('mainMenu');
const SELECTION = document.getElementById('selection');
const OPTION_CONTAINER = document.getElementById('options');
const OPTIONS = [...document.querySelectorAll('.opt')];

let height;

function addEventListeners() {
  OPTIONS.forEach((option, index) => {
    option.addEventListener('click', (e) => {
      let val = option.innerText;
      SELECTION.innerText = val;
      positionMenu(index);

      if (MENU.classList.contains('is-open')) {
        MENU.classList.remove('is-open')
      }
    })
  })

  MENU.addEventListener('mouseenter', () => {
    MENU.classList.add('is-open');
  })

  MENU.addEventListener('mouseleave', () => {
    MENU.classList.remove('is-open');
  })
}

function getOptionHeight() {
  height = OPTIONS[0].getBoundingClientRect().height;
}

function positionMenu(index) {
  index--;
  OPTION_CONTAINER.style.transform = `translateY(-${index * height}px)`;
}

getOptionHeight();
addEventListeners();
* {
  box-sizing: border-box;
  cursor: default;
  user-select: none;
  font-family: Roboto;
  font-weight: 400;
}

.settings {
  /* demo */
  margin: 100px 0 100px;
  
  padding: 7px 16px;
  border: 0.5px solid #7777;
  border-radius: 4px;
}

.options-container {
  position: relative;
  width: 200px;
}

.options {
  display: inline-flex;
  flex-flow: column wrap;
  justify-content: flex-start;
  align-items: stretch;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  opacity: 0;
  pointer-events: none;
}

.opt {
  padding: 5px 20px;
  flex-grow: 1;
  margin: 0;
  background: #777;
  color: #FFF;
  text-align: center;
  display: inline-block;
  width: 100%;
}

.selected {
  background: #28F;
}

.options-container.is-open .options {
  opacity: 1;
  pointer-events: auto;
}

.opt:hover {
  background: #28F;
}
<div class='settings'>
  <p>Select default browser</p>
  <div id='mainMenu' class="options-container">
    <span id="selection" class="opt selected">Select</span>
    <div class="options" id="options">
      <span class='opt'>Google Chrome</span>
      <span class='opt'>Mozilla Firefox</span>
      <span class='opt'>Safari</span>
      <span class='opt'>Microsoft Edge</span>
      <span class='opt'>Opera</span>
      <span class='opt'>Internet Explorer</span>
    </div>
  </div>
</div>

Upvotes: 1

evgeni fotia
evgeni fotia

Reputation: 4810

I added a hover effect so that you can see that .setting height is dynamic

var mainMenu = document.getElementById("mainMenu");
var heightc = document.getElementById("heightc");
var opt = document.getElementsByClassName("opt");
var scrollHeight = 0;
mainMenu.addEventListener('mouseenter', function(){
    for(var i=0; i<opt.length; i++){
       if(opt[i].className.indexOf("selected") > 0){
          var dm = i*opt[i].offsetHeight;
          mainMenu.style.bottom = dm + 'px';
          heightc.style.paddingBottom = dm + 'px';
          scrollHeight = window.scrollY;
          window.scrollTo(0, window.scrollY+dm);
       }
    }
});


mainMenu.addEventListener('mouseleave', function(){
    mainMenu.style.bottom = 0;
    heightc.style.paddingBottom = 0;
    window.scrollTo(0, scrollHeight); 
});
* {
	box-sizing: border-box;
	cursor: default;
	user-select: none;
	font-family: Roboto;
	font-weight: 400;
}
#fs{
  height: 1000px;
}
.settings {
	padding: 7px 16px;
	border: 0.5px solid #7777;
	border-radius: 4px;
}
#mainMenu {
	display: inline-flex;
	flex-flow: column wrap;
	justify-content: flex-start;
	align-items: stretch;
  position: relative;
}
/* added */
#mainMenu span{
  display: none;
}
#mainMenu .selected{
  display: block;
}
#mainMenu:hover .opt{
  display: block;
}
/* end added */
span.opt {
	padding: 5px 12px;
	flex-grow: 1;
	margin: 0;
	background: #777;
	color: #FFF;
	text-align: center;
}
span.selected {
	display: inline-block !important;
	background: #28F;
}
<div class='settings'>
  <p id="heightc">Select default browser</p>
  <span id='mainMenu'>
    <span class='opt'>Google Chrome</span>
    <span class='opt'>Mozilla Firefox</span>
    <span class='opt'>Safari</span>
    <span class='opt selected'>Microsoft Edge</span>
    <span class='opt'>Opera</span>
    <span class='opt'>Internet Explorer</span>
  </span>
</div>
<div id="fs"></div>

Upvotes: 2

Related Questions