Czy znasz to uczucie, gdy tworzysz własny komponent zamiast ulepszać domyślne komponenty HTML? Twój zespół projektowy stworzył coś pięknego, ale przeglądarki nie będą tego obsługiwać od razu, a naprawa wszędzie staje się koszmarem. Wszyscy znamy ten ból, ale te wyzwania sprawiają, że nasza praca jest interesująca.
Dziś chciałbym omówić jedną pułapkę, jaka na nas czyha podczas tej ekscytującej podróży: rozmieszczenie elementów rozwijanych, takich jak menu wyboru czy selektor daty.
Całkowicie źle
Na pierwszy rzut oka wygląda na to, że position: absolute
rozwiązuje wszystkie nasze problemy i w pewnym stopniu tak jest. Ale potem okna modalne wszystko psują.
Jeśli rozwijana lista się przepełni, zostanie ona odcięta. Jasne, możesz przewinąć w dół i zobaczyć ją, ale lepiej módl się, aby projektant nie mógł dosięgnąć cię ostrym przedmiotem.
Daleko nam do ideału — możemy zrobić to lepiej.
Napraw to za pomocą 'fixed'
Jeśli chcemy wyświetlić zawartość na wszystkim, potrzebujemy position: fixed
. Jedynym problemem jest to, że stracimy współrzędne elementu nadrzędnego: elementy fixed są z natury dość niezależne. Oznacza to, że jedyne, co musimy zrobić, to określić dokładne współrzędne elementu rozwijanego w następujących sytuacjach:
- Kiedy to wyświetlamy.
- Gdy jego zawartość ulegnie zmianie.
- Kiedy przewijamy okno i/lub przewijalny element nadrzędny.
- Gdy zmieniamy rozmiar okna i/lub przewijalnego elementu nadrzędnego.
Musimy również zdecydować, czy wyświetlić go nad przełącznikiem, jeśli jest zbyt blisko dołu ekranu. Wydaje się to wykonalne.
Użyję Vue.js, ale powinno być ono łatwe do zrozumienia nawet jeśli wolisz Reacta lub Angulara.
Rozbujajmy to połączenie
Oto struktura, którą wykorzystamy:
export const useDropdownAttributes = () => { const dropdownWidth = ref(''); const dropdownTop = ref(''); const dropdownBottom = ref(''); const dropdownLeft = ref(''); const isDirectedUpwards = ref(false); const togglerRect = ref<DOMRect>(); const dropdownRect = ref<DOMRect>(); const autodetectPosition = ( isDropdownDisplayed: Ref<boolean>, togglerElement: HTMLElement | null = null, dropdownElement: HTMLDivElement | null = null, dropdownContent: Ref<unknown> | ComputedRef<unknown> = ref([]), isUpwardPreferred = false, ) => { // ... } return { autodetectPosition, dropdownTop, dropdownBottom, dropdownLeft, dropdownWidth, isDirectedUpwards, togglerRect, dropdownRect, }; };
Istnieją cztery zmienne dla pozycji rozwijanej, a także flaga isDirectedUpwards
i funkcja, która je wszystkie aktualizuje. Zwracamy również dwie zmienne dla prostokątów przełącznika i rozwijanej listy: może to być wygodne, na przykład dla podpowiedzi, które muszą być wyrównane do środka zawartości.
Jak zapewne pamiętasz, musimy także obsłużyć przewijanie i zmianę rozmiaru przewijalnego elementu nadrzędnego, więc utwórzmy funkcję, która je znajdzie:
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); };
Teraz dodajmy funkcję główną:
const autodetectPosition = ( isDropdownDisplayed: Ref<boolean>, togglerElement: HTMLElement | null = null, dropdownElement: HTMLElement | null = null, dropdownContent: Ref<unknown> | ComputedRef<unknown> = 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 }, ); };
Przekazujemy isDropdownDisplayed
i dropdownContent
, abyśmy mogli reagować na ich aktualizacje.
Przekazujemy również togglerElement
i dropdownElement
, których potrzebujemy do obliczenia pozycji.
Na koniec mamy opcję isUpwardPreferred
, jeśli chcesz, aby lista rozwijana domyślnie znajdowała się nad przełącznikiem.
Czas na relaks i przyjemność
W swoim komponencie będziesz potrzebować czegoś takiego (zakładam, że dodałeś odwołania do przełącznika i listy rozwijanej w szablonie):
const { autodetectPosition, dropdownTop, dropdownBottom, dropdownLeft, dropdownWidth, isDirectedUpwards, } = useDropdownAttributes(); const togglerRef = ref<HTMLElement>(); const dropdownRef = ref<HTMLElement>(); const isDropdownShown = ref(false); onMounted(() => { autodetectPosition(isDropdownShown, togglerRef.value?.$el, dropdownRef.value?.$el); });
A kod CSS będzie wyglądał tak:
.dropdown { position: fixed; bottom: v-bind('isDirectedUpwards ? dropdownBottom : dropdownTop'); left: v-bind('dropdownLeft'); width: v-bind('dropdownWidth'); min-width: 0; }
Voilà. Lista rozwijana wyświetla się poprawnie, nawet gdy jest przepełniona i przesuwa się nad przełącznikiem, jeśli nie ma wystarczająco dużo miejsca poniżej.
A skoro jesteśmy już przy końcu artykułu, chciałabym zostawić Was z czymś radosnym — ale nie mam już pomysłów. Obawiam się, że tym razem mogę powiedzieć tylko „powodzenia”. Powodzenia. 👋
Pełny kod znajdziesz na GitHub .