From f3556d7d89b4e7fd27c7db2f185f31f2f4355c7c Mon Sep 17 00:00:00 2001 From: Max Ronzhin Date: Mon, 27 Jul 2026 01:32:02 +0300 Subject: [PATCH] =?UTF-8?q?fix(calendar):=20=D1=83=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=C2=AB=D0=B3.=C2=BB=20=D0=B8=D0=B7=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=BF=D0=B8=D1=81=D0=B5=D0=B9=20=D0=B4=D0=BD=D0=B5=D0=B9?= =?UTF-8?q?=20=D0=B2=20=D1=81=D0=B5=D1=82=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В 0.0.4 «г.» убрали из подписи периода, но дату дня FullCalendar подставляет ещё и в служебную подпись ячейки (`aria-label`, у navLinks — `title`): видна как всплывающая подсказка и в accessibility-дереве. Формат этой подписи библиотека наружу не отдаёт — `buildNavLinkAttrs` форматирует дату жёстко зашитым `{year,month:'long',day:'numeric'}`, а локаль `ru` всегда даёт «20 июля 2026 г.», — поэтому чистим уже отрендеренный атрибут. Чистка идёт через MutationObserver, а не через `dayCellDidMount`/ `dayHeaderDidMount`: при переключении недельного вида на месячный FullCalendar переиспользует смонтированные ячейки, `didMount` для них повторно не зовётся, и «г.» возвращалась в строке переиспользованных ячеек (поймано на живой проверке). Проверено в обоих видах и при навигации вперёд/назад/«Сегодня»: подписей с «г.» в документе не остаётся. --- .../calendar/ConferenceCalendar.tsx | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/calendar/ConferenceCalendar.tsx b/frontend/src/components/calendar/ConferenceCalendar.tsx index 5504826..d761e87 100644 --- a/frontend/src/components/calendar/ConferenceCalendar.tsx +++ b/frontend/src/components/calendar/ConferenceCalendar.tsx @@ -98,6 +98,57 @@ function renderEventContent(arg: EventContentArg) { /** «г.» — сноска Intl.DateTimeFormat('ru-RU', ...) после года при формате year+month(+day); в подписи периода не нужна. */ const YEAR_SUFFIX_RE = /\s*г\.(?=\s|$)/g +/** + * Подписи дней в самой сетке, из которых надо убрать «г.» (см. + * `useStripYearSuffix`): месячный вид — номер дня в ячейке, недельный — + * подпись колонки дня. + */ +const DAY_LABEL_SELECTOR = '.fc-daygrid-day-number, .fc-col-header-cell-cushion' + +/** Убирает «г.» из `aria-label`/`title` элемента; ничего не пишет, если менять нечего. */ +function stripYearSuffixAttrs(el: Element) { + for (const attr of ['aria-label', 'title']) { + const value = el.getAttribute(attr) + if (!value) continue + const cleaned = value.replace(YEAR_SUFFIX_RE, '') + if (cleaned !== value) el.setAttribute(attr, cleaned) + } +} + +/** + * Убирает «г.» из служебных подписей дней внутри сетки FullCalendar. + * + * Кроме подписи периода (её формат мы задаём сами, см. `buildTitleFormatter`), + * дату дня FullCalendar подставляет ещё и в подпись ячейки — она видна в + * accessibility-дереве и как всплывающая подсказка. Формат этой подписи + * библиотека наружу НЕ отдаёт: `buildNavLinkAttrs` (@fullcalendar/core) + * форматирует дату жёстко зашитым `{year,month:'long',day:'numeric'}` и кладёт + * результат в `aria-label` (в `title` — когда включены navLinks), а с локалью + * `ru` Intl всегда даёт «20 июля 2026 г.». Настройкой это не убирается, + * поэтому правим уже отрендеренный атрибут. + * + * Почему MutationObserver, а не хуки `dayCellDidMount`/`dayHeaderDidMount`: + * при переключении недельного вида на месячный FullCalendar ПЕРЕИСПОЛЬЗУЕТ + * уже смонтированные ячейки — `didMount` для них повторно не зовётся, а + * подпись внутри перерисовывается заново, и «г.» возвращается (поймали на + * живой проверке: после переключения вида ровно строка переиспользованных + * ячеек оставалась с «г.»). Наблюдатель же ловит любую перерисовку. + * Зацикливания нет: `stripYearSuffixAttrs` пишет атрибут, только если он + * реально меняется, поэтому вызванная нашей же правкой итерация ничего не + * пишет и цепочка обрывается. + */ +function useStripYearSuffix(rootRef: React.RefObject) { + useEffect(() => { + const root = rootRef.current + if (!root) return + const clean = () => root.querySelectorAll(DAY_LABEL_SELECTOR).forEach(stripYearSuffixAttrs) + clean() + const observer = new MutationObserver(clean) + observer.observe(root, { subtree: true, childList: true, attributeFilter: ['aria-label', 'title'] }) + return () => observer.disconnect() + }, [rootRef]) +} + /** * Форма аргумента, который FullCalendar передаёт функции-`titleFormat` * (`VerboseFormattingArg` из @fullcalendar/core/internal-common — не @@ -152,6 +203,9 @@ export function ConferenceCalendar({ }: ConferenceCalendarProps) { const calendarRef = useRef(null) const titleRef = useRef(null) + const gridRef = useRef(null) + + useStripYearSuffix(gridRef) // См. комментарий у `latestOnEditClick` в начале файла — синхронизируем // переменную вместо того, чтобы замыкать пропс напрямую в renderEventContent. @@ -269,7 +323,7 @@ export function ConferenceCalendar({ -
+