Некоторые сведения о компоненте относятся к его окружению, а не к свойствам: в каком документе он находится, куда монтировать 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 соответствовали окружению компонентов.