fix(calendar): убрать «г.» из подписей дней в сетке
В 0.0.4 «г.» убрали из подписи периода, но дату дня FullCalendar
подставляет ещё и в служебную подпись ячейки (`aria-label`, у navLinks —
`title`): видна как всплывающая подсказка и в accessibility-дереве.
Формат этой подписи библиотека наружу не отдаёт — `buildNavLinkAttrs`
форматирует дату жёстко зашитым `{year,month:'long',day:'numeric'}`, а
локаль `ru` всегда даёт «20 июля 2026 г.», — поэтому чистим уже
отрендеренный атрибут.
Чистка идёт через MutationObserver, а не через `dayCellDidMount`/
`dayHeaderDidMount`: при переключении недельного вида на месячный
FullCalendar переиспользует смонтированные ячейки, `didMount` для них
повторно не зовётся, и «г.» возвращалась в строке переиспользованных ячеек
(поймано на живой проверке).
Проверено в обоих видах и при навигации вперёд/назад/«Сегодня»: подписей с
«г.» в документе не остаётся.
This commit is contained in:
@@ -98,6 +98,57 @@ function renderEventContent(arg: EventContentArg) {
|
|||||||
/** «г.» — сноска Intl.DateTimeFormat('ru-RU', ...) после года при формате year+month(+day); в подписи периода не нужна. */
|
/** «г.» — сноска Intl.DateTimeFormat('ru-RU', ...) после года при формате year+month(+day); в подписи периода не нужна. */
|
||||||
const YEAR_SUFFIX_RE = /\s*г\.(?=\s|$)/g
|
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<HTMLElement | null>) {
|
||||||
|
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`
|
* Форма аргумента, который FullCalendar передаёт функции-`titleFormat`
|
||||||
* (`VerboseFormattingArg` из @fullcalendar/core/internal-common — не
|
* (`VerboseFormattingArg` из @fullcalendar/core/internal-common — не
|
||||||
@@ -152,6 +203,9 @@ export function ConferenceCalendar({
|
|||||||
}: ConferenceCalendarProps) {
|
}: ConferenceCalendarProps) {
|
||||||
const calendarRef = useRef<FullCalendar | null>(null)
|
const calendarRef = useRef<FullCalendar | null>(null)
|
||||||
const titleRef = useRef<HTMLSpanElement | null>(null)
|
const titleRef = useRef<HTMLSpanElement | null>(null)
|
||||||
|
const gridRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useStripYearSuffix(gridRef)
|
||||||
|
|
||||||
// См. комментарий у `latestOnEditClick` в начале файла — синхронизируем
|
// См. комментарий у `latestOnEditClick` в начале файла — синхронизируем
|
||||||
// переменную вместо того, чтобы замыкать пропс напрямую в renderEventContent.
|
// переменную вместо того, чтобы замыкать пропс напрямую в renderEventContent.
|
||||||
@@ -269,7 +323,7 @@ export function ConferenceCalendar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="vc-calendar" aria-busy={isLoading}>
|
<div className="vc-calendar" aria-busy={isLoading} ref={gridRef}>
|
||||||
<FullCalendar
|
<FullCalendar
|
||||||
ref={calendarRef}
|
ref={calendarRef}
|
||||||
plugins={CALENDAR_PLUGINS}
|
plugins={CALENDAR_PLUGINS}
|
||||||
|
|||||||
Reference in New Issue
Block a user