From 6ff946dc679c71646660269aae94855fc07fffa3 Mon Sep 17 00:00:00 2001 From: Leonardo Cavone Date: Wed, 8 Jul 2026 18:08:44 +0200 Subject: [PATCH] Fixed table, th HTML markup --- README.md | 2 +- package-lock.json | 4 +- package.json | 2 +- projects/fullcalendar/README.md | 2 +- projects/fullcalendar/package.json | 2 +- .../src/lib/fullcalendar-options.ts | 2 +- .../src/lib/fullcalendar.component.ts | 68 +++++++++++++++++++ tsconfig.json | 1 + 8 files changed, 76 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 707ae59..e9f295d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A FullCalendar component for Angular. ## Supported Angular Versions -`@talentia/fullcalendar` version `4.4.14` supports Angular 18. +`@talentia/fullcalendar` version `4.4.15` supports Angular 18. # FullCalendar Angular Component [![Build Status](https://travis-ci.com/fullcalendar/fullcalendar-angular.svg?branch=master)](https://travis-ci.com/fullcalendar/fullcalendar-angular) diff --git a/package-lock.json b/package-lock.json index 5c55b36..465c71d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fullcalendar", - "version": "4.4.14", + "version": "4.4.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fullcalendar", - "version": "4.4.14", + "version": "4.4.15", "dependencies": { "@angular/animations": "20.3.18", "@angular/common": "20.3.18", diff --git a/package.json b/package.json index 0a75310..45465b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fullcalendar", - "version": "4.4.14", + "version": "4.4.15", "scripts": { "ng": "ng", "build": "npm-run-all -s clean:build build:prod", diff --git a/projects/fullcalendar/README.md b/projects/fullcalendar/README.md index 707ae59..e9f295d 100644 --- a/projects/fullcalendar/README.md +++ b/projects/fullcalendar/README.md @@ -5,7 +5,7 @@ A FullCalendar component for Angular. ## Supported Angular Versions -`@talentia/fullcalendar` version `4.4.14` supports Angular 18. +`@talentia/fullcalendar` version `4.4.15` supports Angular 18. # FullCalendar Angular Component [![Build Status](https://travis-ci.com/fullcalendar/fullcalendar-angular.svg?branch=master)](https://travis-ci.com/fullcalendar/fullcalendar-angular) diff --git a/projects/fullcalendar/package.json b/projects/fullcalendar/package.json index f3c2070..e88bd09 100644 --- a/projects/fullcalendar/package.json +++ b/projects/fullcalendar/package.json @@ -1,6 +1,6 @@ { "name": "@talentia/fullcalendar", - "version": "4.4.14", + "version": "4.4.15", "title": "FullCalendar Angular Component", "description": "A FullCalendar component for Angular", "license": "MIT", diff --git a/projects/fullcalendar/src/lib/fullcalendar-options.ts b/projects/fullcalendar/src/lib/fullcalendar-options.ts index a6dfbc0..9dae83a 100644 --- a/projects/fullcalendar/src/lib/fullcalendar-options.ts +++ b/projects/fullcalendar/src/lib/fullcalendar-options.ts @@ -115,7 +115,7 @@ export const INPUT_NAMES = [ 'forceEventDuration', 'progressiveEventRendering', 'selectLongPressDelay', - 'selectMinDistance', + // 'selectMinDistance', 'timeZoneParam', 'titleRangeSeparator', // compound OptionsInput... diff --git a/projects/fullcalendar/src/lib/fullcalendar.component.ts b/projects/fullcalendar/src/lib/fullcalendar.component.ts index d69b155..2786d6f 100644 --- a/projects/fullcalendar/src/lib/fullcalendar.component.ts +++ b/projects/fullcalendar/src/lib/fullcalendar.component.ts @@ -36,10 +36,62 @@ export class FullCalendarComponent implements AfterViewInit, DoCheck, OnChanges, private calendar: Calendar; private dirtyProps: any = {}; private deepCopies: any = {}; + private a11yObserver?: MutationObserver; + private a11yPatchScheduled = false; + private initialRenderInProgress = true; ngAfterViewInit() { this.calendar = new Calendar(this.element.nativeElement, this.buildOptions()); this.calendar.render(); + this.initialRenderInProgress = false; + this.patchAccessibility(); + + // Primary trigger: datesRender/viewSkeletonRender (wired in buildOptions) fire whenever + // FullCalendar (re)builds the header/body table skeleton, which is cheaper and more + // targeted than scanning on every DOM mutation. + // + // Fallback: a MutationObserver, in case some table rebuild path doesn't go through those + // hooks. Its callback only re-patches when an added node actually looks like calendar + // table markup, instead of unconditionally rescanning the whole tree on any mutation. + // Attribute writes done by patchAccessibility don't trigger childList mutations, so this + // cannot loop on itself. + this.a11yObserver = new MutationObserver((mutations) => { + const hasTableMarkup = mutations.some((mutation) => + Array.from(mutation.addedNodes).some((node) => + node instanceof Element && (node.matches('table, th') || !!node.querySelector('table, th')) + ) + ); + if (hasTableMarkup) this.scheduleAccessibilityPatch(); + }); + this.a11yObserver.observe(this.element.nativeElement, { childList: true, subtree: true }); + } + + private scheduleAccessibilityPatch() { + if (this.a11yPatchScheduled) return; + this.a11yPatchScheduled = true; + queueMicrotask(() => { + this.a11yPatchScheduled = false; + this.patchAccessibility(); + }); + } + + /* + Fix: FullCalendar (v4 through at least v6) never declares its day-grid/timeline + markup as data tables with associated headers, nor marks the purely positional ones as + presentational. Two independent tables are involved: the header row (
, e.g. + fc-day-header in dayGrid, unclassed in resource-timeline) and the per-week/day/resource body + grids ( with
cells only, no at all). Neither carries scope/role, so this patches + both after every render. All observed are column headers (no scope="row" case found across + dayGrid/timeGrid/resource-timeline views), hence the unqualified selector below. + */ + private patchAccessibility() { + const root: HTMLElement = this.element.nativeElement; + root.querySelectorAll('th:not([scope])').forEach((th) => th.setAttribute('scope', 'col')); + root.querySelectorAll('table:not([role])').forEach((table) => { + if (!table.querySelector('th')) { + table.setAttribute('role', 'presentation'); + } + }); } private buildOptions() { @@ -51,6 +103,20 @@ export class FullCalendarComponent implements AfterViewInit, DoCheck, OnChanges, }; }); + // datesRender/viewSkeletonRender fire whenever FullCalendar (re)builds the header/body + // table skeleton, so they're the cheapest, most targeted trigger for the a11y patch — + // wrap rather than replace so consumers still receive the event. Skipped during the + // initial render() call since ngAfterViewInit already patches synchronously right after it. + ['datesRender', 'viewSkeletonRender'].forEach(outputName => { + const emit = options[outputName]; + options[outputName] = (...args) => { + if (!this.initialRenderInProgress) { + this.scheduleAccessibilityPatch(); + } + emit(...args); + }; + }); + // do after outputs, so that inputs with same name override INPUT_NAMES.forEach(inputName => { let inputVal = this[inputName]; @@ -121,6 +187,8 @@ export class FullCalendarComponent implements AfterViewInit, DoCheck, OnChanges, } ngOnDestroy() { + this.a11yObserver?.disconnect(); + this.a11yObserver = undefined; if (this.calendar) { this.calendar.destroy(); } diff --git a/tsconfig.json b/tsconfig.json index f4fdc11..1a3a241 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "./dist/fullcalendar" ] }, + "rootDir": "./projects/fullcalendar/src", "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true,