Appropriate view of specially opened items: how to make a guide

Do you know this feeling when you create your own component instead of developing the default HTML components? Your design team has created something nice, but the browsers will not take it out of the box, and it becomes a nightmare to fix it anywhere. We all know this pain, but these difficulties make our work interesting.
Today, I wanted to talk about a trap waiting for us during this exciting journey: Placing elements such as selection menus or history selections.
Absolutely wrong
Looks like at first position: absolute
Solves all our problems and to some extent. But then, the modal windows ruin everything.
If it opens, it is cut. Of course, you can scroll down and see, but it would be better if you pray that your designer cannot reach you with a sharp object.
This is far from perfect – we can do better.
Correct with 'fixed'
If we want to view the content on everything, position: fixed
. The only problem is that we will lose the main element coordinates: Fixed elements are quite independent compared to nature. This is the only thing we need to do, in these cases the exact coordinates of the opens item:
- When we view it.
- When its content changes.
- When we scroll the window and/or shifted parent.
- When we resize the window and/or shifted parent.
Also, if it is too close to the bottom of the screen, we have to decide whether Toggler will be displayed on it. Makes you feel.
I will use VUE.JS, but even if you prefer reaction or angular, it should be easy to follow.
Let’s shake this joint
Here is the structure we will use:
export const useDropdownAttributes = () => {
const dropdownWidth = ref('');
const dropdownTop = ref('');
const dropdownBottom = ref('');
const dropdownLeft = ref('');
const isDirectedUpwards = ref(false);
const togglerRect = ref();
const dropdownRect = ref();
const autodetectPosition = (
isDropdownDisplayed: Ref,
togglerElement: HTMLElement | null = null,
dropdownElement: HTMLDivElement | null = null,
dropdownContent: Ref | ComputedRef = ref([]),
isUpwardPreferred = false,
) => {
// ...
}
return {
autodetectPosition,
dropdownTop,
dropdownBottom,
dropdownLeft,
dropdownWidth,
isDirectedUpwards,
togglerRect,
dropdownRect,
};
};
Four variables for the opening location and isDirectedUpwards
Flag and a function that updates them all. In addition, we return two variables to the Toggler and the pop -up menu: for example, it may be suitable for vehicle tips that need to be aligned in the middle of the content.
As you may recall, we need to consider the sliding and resizing of the shifable parent, so let’s create a function to find it:
const getFirstScrollableParent = (element: HTMLElement | null): HTMLElement => {
const parentElement = element?.parentElement;
if (!parentElement) return document.body;
const overflowY = window.getComputedStyle(parentElement).overflowY;
if (overflowY === 'scroll' || overflowY === 'auto') return parentElement;
return getFirstScrollableParent(parentElement);
};
Now let’s add the main function:
const autodetectPosition = (
isDropdownDisplayed: Ref,
togglerElement: HTMLElement | null = null,
dropdownElement: HTMLElement | null = null,
dropdownContent: Ref | ComputedRef = ref([]),
isUpwardPreferred = false,
) => {
if (!togglerElement || !dropdownElement) return;
const updateDropdownAttributes = () => {
togglerRect.value = togglerElement.getBoundingClientRect();
dropdownRect.value = dropdownElement.getBoundingClientRect();
dropdownWidth.value = `${togglerRect.value.width}px`;
dropdownBottom.value = `${window.innerHeight - togglerRect.value.top}px`;
dropdownTop.value = `${
window.innerHeight - togglerRect.value.bottom - dropdownRect.value.height
}px`;
dropdownLeft.value = `${togglerRect.value.left}px`;
};
const handleResize = () => {
requestAnimationFrame(updateDropdownAttributes);
};
const handleScroll = () => {
requestAnimationFrame(updateDropdownAttributes);
};
watch(
[isDropdownDisplayed, dropdownContent],
([newVal, _]) => {
const scrollableParent = getFirstScrollableParent(togglerElement);
if (!newVal) {
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll);
scrollableParent.removeEventListener('resize', handleResize);
scrollableParent.removeEventListener('scroll', handleScroll);
return;
}
requestAnimationFrame(() => {
const distanceFromBottom =
window.innerHeight - togglerElement.getBoundingClientRect().bottom;
const distanceFromTop = togglerElement.getBoundingClientRect().top;
const dropdownHeight = dropdownElement.offsetHeight;
isDirectedUpwards.value = isUpwardPreferred
? distanceFromTop > dropdownHeight
: distanceFromBottom < dropdownHeight &&
distanceFromTop > dropdownHeight;
updateDropdownAttributes();
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll);
scrollableParent.addEventListener('resize', handleResize);
scrollableParent.addEventListener('scroll', handleScroll);
});
},
{ deep: true },
);
};
We pass isDropdownDisplayed
And dropdownContent
So we can react to updates.
We are passing too togglerElement
And dropdownElement
We need to calculate the position.
Finally exists isUpwardPreferred
If you want a default list on Toggler.
Time to relax and enjoy
In your component, you will need something like this (I assume that you add to the template tooggler and opening rev):
const {
autodetectPosition,
dropdownTop,
dropdownBottom,
dropdownLeft,
dropdownWidth,
isDirectedUpwards,
} = useDropdownAttributes();
const togglerRef = ref();
const dropdownRef = ref();
const isDropdownShown = ref(false);
onMounted(() => {
autodetectPosition(isDropdownShown, togglerRef.value?.$el, dropdownRef.value?.$el);
});
And CSS will appear as follows:
.dropdown {
position: fixed;
bottom: v-bind('isDirectedUpwards ? dropdownBottom : dropdownTop');
left: v-bind('dropdownLeft');
width: v-bind('dropdownWidth');
min-width: 0;
}
Voilà. If there is not enough space below, it opens even when moving on the overflow and togger.
And because we were at the end of the article, I would like to leave you with something cheerful – but I have exhausted ideas. I’m afraid, this time the only thing I have “good luck”. Good glory. 👋
You can find full code in Github.