Перейти к содержанию
Mercury
Русский
Дизайн книга
Меню
Русский

UI1: окружение поддерева

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

Некоторые сведения о компоненте относятся к его окружению, а не к свойствам: в каком документе он находится, куда монтировать popover, в какую сторону идёт текст и на каком языке подписи. Mercury UI хранит всё это в одном наследуемом окружении. Компонент читает его; три провайдера задают.

Единое окружение

Окружение — контекст из @fiberfx/core; задаётся через EnvironmentProvider из @fiberfx/ui. Предсказуемость обеспечивают два правила:

  • Переопределения объединяются. Провайдер задаёт только переданные поля. Пропущенное поле наследуется от внешнего провайдера; null означает недоступность ресурса и никогда не заменяется значением по умолчанию.
  • Без провайдера поведение прежнее. Mercury использует значения для одного документа: глобальный document, его body как узел порталов и ltr.

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

Поля EnvironmentProvider

Свойство Тип Если не задано
portalContainer HTMLElement | null body документа-владельца
ownerDocument Document | null документ контейнера, иначе окна, иначе глобальный
ownerWindow Window | null defaultView документа-владельца, иначе глобальное окно
direction "ltr" | "rtl" "ltr"
locale string (BCP 47) локаль среды выполнения
reducedMotion "user" | "always" | "never" "user": следует prefers-reduced-motion окна-владельца
focusFallback элемент, ссылка на него или null нет
idPrefix string нет

Используйте его, когда Mercury рендерится не в основном документе страницы: во встроенном редакторе, iframe предварительного просмотра или острове внутри другого приложения.

  • Целевой узел следует документу. Слушатели закрытия и фокуса подключаются к документу, которому принадлежит наложение. При замене portalContainer с открытым наложением слушатели прежнего документа удаляются до подключения новых.
  • Целевой узел может исчезнуть. После его уничтожения наложение ничего не отображает и не переносится в родительский документ. Если цель возврата фокуса исчезла вместе с ним, фокус переходит в focusFallback.
  • Два корня на одной странице. Каждый остров имеет собственный корень рендеринга и собственную нумерацию id; без префикса два острова начнут одинаково. Задайте каждому корню отдельный idPrefix, чтобы идентификаторы и связи ARIA не пересекались.
  • Серверный рендеринг не обращается к глобальным объектам браузера. Все ресурсы там разрешаются в null, поэтому портал ничего не отображает.

Portal читает целевой узел

Portal монтирует потомков в другом контейнере, позволяя всплывающей поверхности выйти за overflow и контекст наложения места объявления. Он принимает children и необязательный container:

  • с container монтирует туда; container={null} ничего не отображает;
  • без него использует portalContainer окружения;
  • если целевого узла нет, например на сервере, ничего не отображает.

Содержимое портала сохраняет место в дереве компонентов и продолжает читать провайдеры логического родителя, включая окружение. Но события DOM нативные: всплывают через DOM целевого узла, а не к логическому родителю. Наложения Mercury уже используют порталы; прямой Portal нужен только для собственного наложения.

Пример — остров витрины Mercury. Он создаёт узел порталов внутри своего элемента, оборачивает примеры в <EnvironmentProvider portalContainer={portalHost}> и ставит класс темы на этот элемент, а не на <html> основной страницы. Так наложения сохраняют токены острова и не попадают без оформления в body основной страницы.

DirectionProvider

DirectionProvider — краткая запись только для направления: <DirectionProvider dir="rtl"> равнозначен EnvironmentProvider с direction="rtl" и наследованием остальных полей. dir обязателен: "ltr" или "rtl".

useDirection() возвращает действующее направление ближайшего провайдера, иначе "ltr". Его читают компоненты, зеркально меняющие навигацию стрелками или размещение.

Контекст проходит через портал, а атрибут DOM dir — нет. Поэтому направление хранится в окружении: popover в body всё ещё знает, что принадлежит разделу с письмом справа налево.

LocaleProvider

LocaleProvider задаёт локаль поддерева в том же окружении и добавляет каталоги сообщений. Второго хранилища локали нет, поэтому наложение в портале читает ту же локаль, что и родитель.

Свойство Тип Примечание
locale string обязательно, чтобы сервер и клиент выводили одинаковый текст
direction "ltr" | "rtl" | "auto" пропущено: наследуется, смена локали не меняет направление; "auto": определяется по системе письма локали
catalogs MessageCatalogs сообщения по локалям; при отсутствии наследуется
fallbackLocales string[] проверяются после собственной цепочки локали; при отсутствии наследуется

useLocale() возвращает { locale, direction, explicit }. explicit равен false, если ни один провайдер не задал локаль и используется локаль среды выполнения; она может различаться на сервере и клиенте.

Вложите провайдер для локального переопределения: внутренний действует только на своё поддерево.

Сообщения и форматтеры

import { LocaleProvider, useLocaleFormatters, useMessageFormatter } from "@fiberfx/ui";
import type { MessageCatalogs } from "@fiberfx/ui";

const catalogs: MessageCatalogs<"cart.items"> = {
  en: { "cart.items": (v, f) => f.plural(Number(v.count), { one: "# item", other: "# items" }) },
};

function CartLine({ count, total }: { count: number; total: number }) {
  const f = useLocaleFormatters({ numberOptions: { style: "currency", currency: "EUR" } });
  const t = useMessageFormatter();
  return <p>{t.format("cart.items", { count })}, {f.formatNumber(total)}</p>;
}

export function Cart() {
  return (
    <LocaleProvider locale="en-GB" catalogs={catalogs}>
      <CartLine count={3} total={42.5} />
    </LocaleProvider>
  );
}

useLocaleFormatters(options) возвращает набор для действующей локали: formatNumber, formatDate, formatList, plural, pluralCategory, compare, contains, startsWith и endsWith. Параметры: timeZone, numberOptions, dateOptions, listOptions, collationOptions; сравнение по умолчанию использует sensitivity: "base". Передавайте timeZone, если сервер и клиент могут различаться.

useMessageFormatter() возвращает format(key, values), resolve(key, values) и missing():

  • Сообщение — обычная строка или функция (values, formatters) => string. В plural символ # обозначает отформатированное число.
  • Цепочка поиска: точная локаль, её родители (en-GB, затем en), потом каждая резервная локаль и её родители. В примере в en-GB ничего нет, а en, собственный язык локали, предоставляет сообщение.
  • Тексты Mercury: для ключей mercury.close, mercury.clear, mercury.loading, mercury.moreActions и mercury.selectedCount есть встроенный английский текст на случай отсутствия в каталогах.
  • Отсутствующий ключ отображается самим ключом, а не пустой подписью; при разработке выдаётся предупреждение. resolve сообщает источник каждого сообщения: catalog, fallback, bundled или missing.

Чистые фабрики createLocaleFormatters и createMessageFormatter делают то же вне компонента; localeDirection(locale) возвращает "ltr" или "rtl" для тега локали.

Что по-прежнему задаёт приложение

Провайдер не создаёт элемент и не может задать атрибуты. Приложение задаёт lang и dir на собственном корневом элементе, чтобы двунаправленная раскладка браузера и логические свойства CSS соответствовали окружению компонентов.