Стиральная машина LG ошибка UE: что означает и как исправить

Сегодня разбираем материал исходного автора о теме «Выпадающие списки внутри прокручиваемых контейнеров: почему они ломаются и как это исправить». Практический разбор с шагами и примерами, который можно быстро применить в своей работе.


Сценарий, когда таблица данных помещается в прокручиваемый контейнер и включает в себя маленькие выпадающие списки с действиями, повторяется довольно часто. Во многих случаях всё функционирует корректно, пока элементы находятся в изоляции, но стоит только разместить их внутри прокручиваемого div, как начинаются проблемы. Я встречался с этой ошибкой в нескольких кодовых базах, включая контейнеры, стеки и фреймворки — разнообразие реализаций, но с одинаковыми результатами.

Выпадающие списки могут обрезаться по границам контейнера или оказаться позади других элементов, хотя по логике должны отображаться выше. Нередко они функционируют нормально, пока пользователь не начнет прокручивать — и вот в этот момент список исчезает. Установка z-index: 9999 иногда помогает, но не всегда, что указывает на более глубокую природу проблемы.

Эти проблемы вызваны взаимодействием трёх различных систем браузеров: overflow, контексты наложения и содержащие блоки. Большинство разработчиков рассматривают каждую из этих систем изолированно, не понимая, что происходит при их объединении.

Как только вы поймёте, как все три взаимодействуют, режимы отказа перестанут казаться случайными — они становятся предсказуемыми.

Три вещи, которые на самом деле это вызывают

Рассмотрим каждый из этих пунктов подробно.

Проблема overflow

Когда вы устанавливаете overflow: hidden, overflow: scroll или overflow: auto на элемент, браузер обрезает всё, что выходит за его границы, включая абсолютно позиционированных потомков.

.scroll-container { overflow: auto; height: 300px; /* Это обрежет выпадающий список, без вариантов */
}

.dropdown { position: absolute; /* Не важно — всё равно обрезается .scroll-container */
}

Когда я впервые столкнулся с этим, я был удивлён. Я полагал, что position: absolute позволит элементу вырваться за пределы обрезки контейнера, но это оказалось не так.

На практике это означает, что абсолютно позиционированное меню может быть обрезано любым предком, у которого задано значение overflow, отличное от visible, — даже если этот предок не является содержащим блоком для меню. Обрезка и позиционирование — это отдельные системы. Они просто сталкиваются способами, которые выглядят совершенно случайными, пока вы не понимаете обе.

See the Pen [Overflow & Clipping [forked]](https://codepen.io/smashingmag/pen/RNGZNPw) by BboyGT.

Это также не просто визуальная проблема; она затрагивает доступность. Когда выпадающий список обрезается, он всё равно остаётся в DOM. Пользователь, который работает с клавиатурой, может сфокусироваться на нём, хотя фактически его не видит. Я наблюдал, как программы чтения с экрана объявляли пункты меню, которые были недоступны для людей с нормальным зрением. Это различие — реальная проблема, которая зачастую незаметна при визуальной проверке.

Ловушка контекста наложения

Думайте о контексте наложения как о запечатанном слое. Всё, что находится внутри него, отрисовывается вместе, как единый блок. Ничто внутри него не может вырваться выше чего-то снаружи, какое бы значение z-index вы ни использовали.

Дело в том, что многие CSS-свойства создают новый контекст наложения. Я не знал, что половина из них запускает новый контекст, пока не начал отлаживать проблемы с z-index и не пришлось их искать:

  • position со значением z-index, отличным от auto;
  • opacity меньше 1;
  • transform, filter, perspective, clip-path или mask;
  • will-change, ссылающийся на любое из вышеперечисленных;
  • isolation: isolate;
  • contain: layout или paint.

Именно по этой причине z-index: 9999 может не оказывать влияния. Если ваш выпадающий список заперт внутри контекста наложения, который располагается ниже другого контекста, его значение z-index теряет актуальность. Сравнение z-index происходит только среди соседних элементов в пределах одного контекста наложения, что объясняет, как модальное окно с z-index: 1 может оказаться поверх вашего выпадающего списка с z-index: 9999 — они не сопоставляются, поскольку находятся в разных контекстах.

Такую войну z-index никогда не выиграть. Вы сражаетесь не на той арене.

See the Pen [Stacking Contexts [forked]](https://codepen.io/smashingmag/pen/zxKdxGL) by BboyGT.

Неожиданность содержащего блока

Я рано узнал кое-что неудобное о содержащих блоках: абсолютное позиционирование не означает «позиционировать где угодно». Браузер находит ближайшего позиционированного предка и использует его как систему отсчёта для координат и размеров этого элемента.

Если этот предок находится глубоко внутри прокручиваемого контейнера, координаты выпадающего списка вычисляются относительно него. Когда контейнер прокручивается, эти координаты не обновляются. Триггер перемещается. Выпадающий список остаётся на месте.

Почему абсолютное позиционирование не справляется в одиночку

Долгое время position: absolute был моим стандартным ответом для выпадающих списков. В изоляции это работает. Но как только вы помещаете это в реальное приложение, начинаются поломки, которые не кажутся связанными ни с чем, что вы изменили.

В чистом DOM position: absolute работает нормально. Реальные приложения просто более запутаны. Почти всегда в дереве предков найдётся что-то, что создаёт неожиданный контекст наложения или обрезает потомков.

Я столкнулся с этим, когда выпадающий список находился внутри таблицы, которая жила внутри прокручиваемого div, а компонент-карточка где-то выше по дереву имел применённый transform: translateZ(0) в качестве подсказки для GPU-компоузинга. Этот transform создал новый контекст наложения. Выпадающий список оказался заперт ниже всего, что находилось снаружи карточки и имело z-index, отличный от auto. А прокручиваемый контейнер при этом всё равно его обрезал.

Отладка этого ощущалась как следование по следу призраков: три разных предка, три разных режима отказа и один невидимый выпадающий список. Как только я перестал пытаться залатать симптом и начал отслеживать, какой предок был виновен, первопричина стала очевидной.

Решения, которые действительно работают

Вот что на самом деле работает.

Порталы: решение, которое в итоге сработало для меня

То, что в конечном счёте помогло мне, — это полное извлечение выпадающего списка из проблемной части DOM с рендерингом непосредственно как дочернего элемента document.body. В React и Vue это называется порталом (portal). В ванильном JavaScript это просто document.body.appendChild().

Как только элемент оказывается на уровне body, ни одна из проблем с обрезкой предками или контекстами наложения не применяется. Выпадающий список находится вне всего этого, и z-index работает так, как вы и ожидаете.

Вот пример на React с использованием createPortal:

import { createPortal } from 'react-dom';
import { useState, useEffect, useRef } from 'react';

function Dropdown({ anchorRef, isOpen, children }) { const [position, setPosition] = useState({ top: 0, left: 0 });

useEffect(() => { if (isOpen && anchorRef.current) { const rect = anchorRef.current.getBoundingClientRect(); setPosition({ top: rect.bottom + window.scrollY, left: rect.left + window.scrollX, }); } }, [isOpen, anchorRef]);

if (!isOpen) return null;

return createPortal( <div id="dropdown-demo" role="menu" className="dropdown-menu" style={{ position: 'absolute', top: position.top, left: position.left }} > {children} </div>, document.body );
}

See the Pen [Portal Fix [forked]](https://codepen.io/smashingmag/pen/Kwgvwdv) by BboyGT.

В моём случае исправление потребовало явной работы с доступностью. Когда я вынес меню через портал за пределы DOM, чтобы избежать обрезки, мне также пришлось восстановить логическую связь для пользователей клавиатуры и программ чтения с экрана, перемещать фокус в меню при его открытии и надёжно возвращать фокус к триггеру при закрытии. Этот дополнительный фрагмент JavaScript устраняет пробел в доступности, который создаёт портал:

<button id="dropdown-toggle" aria-haspopup="menu" aria-expanded="false" aria-controls="dropdown-demo"
> Actions
</button>

<ul id="dropdown-demo" role="menu" hidden> <li role="menuitem">Edit</li> <li role="menuitem">Duplicate</li> <li role="menuitem">Delete</li>
</ul>

Порталы быстро устранили проблему обрезки, но у них есть компромиссы, и я узнал об этом на собственном опыте. В одном репозитории выпадающий список потерял контекст темы, потому что рендерился за пределами провайдера. В другом анимация закрытия выглядела оторванной, потому что события маршрутизировались иначе. Каждый случай потребовал небольшого точечного исправления — проброса контекста, явного восстановления фокуса или переноса анимации в портал. Вместе они показывают, что порталы — это хирургический инструмент, а не универсальная замена одним кликом.

Фиксированное позиционирование (и почему оно сложнее, чем кажется)

Фиксированное позиционирование может казаться простым решением. Вместо того чтобы позиционироваться относительно предка, элемент позиционируется относительно самого вьюпорта (viewport). Но трансформации и другие свойства, как мы видели ранее, создают содержащие блоки, которые могут помешать элементу с position: fixed выйти за пределы контейнера.

.dropdown-menu { position: fixed; /* Координаты задаются через JavaScript */
}
function positionDropdown(trigger, dropdown) { const rect = trigger.getBoundingClientRect(); dropdown.style.top = `${rect.bottom}px`; dropdown.style.left = `${rect.left}px`;
}

При отладке я обнаружил трансформацию на предке, которая перехватила содержащий блок, — это объясняло, почему меню вело себя так, будто было зафиксировано на месте, несмотря на то что формально было fixed.

И, конечно, нельзя игнорировать доступность. Фиксированные элементы, появляющиеся поверх контента, всё равно должны быть доступны с клавиатуры. Если порядок фокуса не перемещается естественным образом в фиксированный выпадающий список, вам придётся управлять этим с помощью кода. Также стоит проверить, что он не перекрывает другой интерактивный контент без возможности его закрыть — это всплывает при тестировании с клавиатурой.

CSS Anchor Positioning: куда, по моему мнению, всё движется

CSS Anchor Positioning — это направление, которое меня сейчас интересует больше всего. Когда я впервые посмотрел на него, я не был уверен, насколько спецификация реально применима. Он позволяет объявить связь между выпадающим списком и его триггером прямо в CSS, а браузер сам обрабатывает координаты.

.trigger { anchor-name: --my-trigger;
}

.dropdown-menu { position: absolute; position-anchor: --my-trigger; top: anchor(bottom); left: anchor(left); position-try-fallbacks: flip-block, flip-inline;
}

Свойство position-try-fallbacks — это то, что делает данный подход предпочтительнее ручных вычислений. Браузер пробует альтернативные варианты размещения перед тем, как сдаться, поэтому выпадающий список у нижнего края вьюпорта автоматически переворачивается вверх, а не обрезается.

Поддержка браузеров хорошая в браузерах на основе Chromium и растёт в Safari. Firefox требует полифила. Пакет @oddbird/css-anchor-positioning покрывает основную спецификацию. Я сталкивался с крайними случаями в вёрстке, которые потребовали непредвиденных запасных вариантов, поэтому относитесь к нему как к прогрессивному улучшению или используйте его в паре с JavaScript-запасным вариантом для Firefox.

Коротко: перспективно, но пока не универсально. Тестируйте в целевых браузерах.

Что касается доступности, объявление визуальной связи в CSS ничего не сообщает дереву доступности. aria-controls, aria-expanded, aria-haspopup — это по-прежнему ваша ответственность.

Иногда решение — просто переместить элемент

Прежде чем прибегать к порталу или выполнять вычисления координат, я всегда задаю один вопрос: действительно ли этот выпадающий список должен находиться внутри прокручиваемого контейнера?

Если нет, перемещение разметки в обёртку более высокого уровня полностью устраняет проблему — без JavaScript и без вычислений координат.

Это не всегда возможно. Если кнопка и выпадающий список инкапсулированы в одном компоненте, перемещение одного без другого означает переосмысление всего API. Но когда это можно сделать, отлаживать нечего — проблема просто не существует.

Что современный CSS всё ещё не решает

CSS прошёл долгий путь в этой области, но есть места, где он всё ещё подводит.

Проблемы с position: fixed и transform по-прежнему существуют. Это намеренно заложено в спецификации, а значит, CSS-обходного пути не существует. Если вы используете библиотеку анимаций, которая оборачивает вашу вёрстку в трансформированный элемент, вы снова возвращаетесь к необходимости использовать порталы или anchor positioning.

CSS Anchor Positioning перспективен, но нов. Как упоминалось ранее, на момент написания этой статьи Firefox всё ещё требует полифила. Я сталкивался с крайними случаями в вёрстке, которые потребовали непредвиденных запасных вариантов. Если вам нужно согласованное поведение во всех браузерах сегодня, для сложных случаев вы всё равно обращаетесь к JavaScript.

Дополнение, которое действительно изменило мой рабочий процесс, — это HTML Popover API, теперь доступный во всех современных браузерах. Элементы с атрибутом popover рендерятся в верхнем слое браузера, поверх всего, без необходимости позиционирования через JavaScript.

<button popovertarget="dropdown-demo">Open</button>
<div id="dropdown-demo" popover="manual" role="menu">Popover content</div>

Обработка нажатия Escape, закрытие по клику вне элемента и надёжная семантика доступности предоставляются бесплатно для таких вещей, как подсказки, виджеты раскрытия и простые оверлеи. Теперь это первый инструмент, к которому я обращаюсь.

При этом он не решает проблему позиционирования — он решает проблему слоёв. Вам всё равно нужен anchor positioning или JavaScript, чтобы выровнять popover относительно его триггера. Popover API управляет слоями. Anchor positioning управляет размещением. Используемые вместе, они покрывают большую часть того, для чего раньше приходилось обращаться к библиотеке.

Руководство по выбору решения для вашей ситуации

После того как я прошёл через всё это на собственном опыте, вот как я теперь думаю о выборе подхода.

Используйте портал, когда триггер находится глубоко во вложенных прокручиваемых контейнерах. Этот паттерн хорошо работает для контекстных меню таблиц в сочетании с восстановлением фокуса и проверками доступности. Это наиболее надёжный вариант, но заложите время на дополнительную обвязку.

Используйте фиксированное позиционирование, когда вы работаете с ванильным JavaScript или лёгким фреймворком и можете убедиться, что ни один предок не применяет трансформации или фильтры. Его просто настроить и просто отлаживать — при условии, что это единственное ограничение соблюдается.

Используйте CSS Anchor Positioning, когда поддержка браузеров позволяет. Если требуется поддержка Firefox, используйте его в паре с полифилом @oddbird. Именно в этом направлении движется платформа, и со временем этот подход станет основным.

Реструктурируйте DOM, когда архитектура позволяет это сделать и вы хотите получить нулевую сложность во время выполнения. Это, пожалуй, самый недооценённый вариант.

Комбинируйте паттерны, когда хотите использовать anchor positioning как основной подход в паре с JavaScript-фолбэком для неподдерживаемых браузеров. Или портал для размещения в DOM в паре с getBoundingClientRect() для точности координат.

Раньше я воспринимал этот баг как разовую проблему — что-то, что нужно залатать и двигаться дальше. Но когда я достаточно долго разбирался с ним, чтобы понять все три задействованные системы — обрезку переполнения (overflow clipping), контексты наложения (stacking contexts) и содержащие блоки (containing blocks) — он перестал казаться случайным. Я мог посмотреть на сломанный выпадающий список и сразу определить, какой предок был виновен. Именно этот сдвиг в том, как я читаю DOM, и стал главным выводом.

Единственно правильного ответа не существует. То, к чему я обращался, зависело от того, что я мог контролировать в кодовой базе: порталы — когда дерево предков было непредсказуемым; фиксированное позиционирование — когда оно было чистым и простым; перемещение элемента — когда ничто не мешало; и anchor positioning — там, где это возможно сейчас.

Что бы вы в итоге ни выбрали, не относитесь к доступности как к последнему шагу. По моему опыту, именно тогда её и пропускают. ARIA-связи, управление фокусом, поведение с клавиатурой — это не полировка. Это часть того, что делает вещь по-настоящему работающей.

Дополнительные материалы

Это ресурсы, к которым я постоянно возвращался в процессе работы над этой темой:

  • The Stacking Context (MDN)
  • «CSS Anchor Positioning Guide», Хуан Диего Родригес
  • «Getting Started With The Popover API», Годстайм Абуру
  • Floating UI (floating-ui.com)
  • CSS Overflow (MDN)

Ответы на эти вопросы могут быть для вас полезными

Почему z-index: 9999 не помогает, когда выпадающий список перекрывается другим элементом?

Потому что z-index сравнивается только между элементами в одном контексте наложения. Если ваш выпадающий список заперт внутри контекста наложения, который отрисовывается ниже другого, никакое значение z-index не поможет — нужно либо убрать лишний контекст наложения у предка, либо вынести список через портал.

Какое свойство CSS чаще всего неожиданно создаёт новый контекст наложения?

На практике чаще всего виновен transform — особенно transform: translateZ(0), который разработчики добавляют как подсказку для GPU-компоузинга. Также часто встречаются opacity < 1, filter и will-change.

Когда стоит использовать HTML Popover API вместо портала на JavaScript?

Popover API подходит для простых оверлеев, подсказок и виджетов раскрытия, где важна встроенная обработка Escape и клика вне элемента. Для сложных выпадающих списков с точным позиционированием относительно триггера всё равно потребуется anchor positioning или getBoundingClientRect() — Popover API решает проблему слоёв, но не позиционирования.

Что делать, если CSS Anchor Positioning не поддерживается в целевых браузерах?

Используйте пакет @oddbird/css-anchor-positioning как полифил для Firefox. Для продакшн-проектов, где нужна стабильность во всех браузерах прямо сейчас, надёжнее комбинировать портал с getBoundingClientRect() для вычисления координат.

Почему перемещение выпадающего списка выше по DOM-дереву — недооценённое решение?

Потому что оно устраняет проблему полностью, без JavaScript и без вычислений координат. Единственное ограничение — архитектурное: если кнопка и меню инкапсулированы в одном компоненте, разделить их без переосмысления API сложно. Но когда это возможно, это самый чистый вариант.

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x