Erstellen von Kalendern mit Blick auf Barrierefreiheit und Internationalisierung

Erstellen von Kalendern mit Blick auf Barrierefreiheit und Internationalisierung

Eine schnelle Suche hier auf CSS-Tricks zeigt, wie viele verschiedene Möglichkeiten es gibt, sich mit Kalendern zu befassen. Manche zeigen wie CSS Grid kann das Layout effizient erstellen. Einige versuchen es aktuelle Daten in die Mischung einbringen. Etwas auf einen Rahmen verlassen bei der Staatsführung zu helfen.

Beim Erstellen einer Kalenderkomponente gibt es viele Überlegungen – weit mehr als das, was in den Artikeln behandelt wird, die ich verlinkt habe. Wenn Sie darüber nachdenken, sind Kalender voller Nuancen, von der Handhabung von Zeitzonen und Datumsformaten bis hin zur Lokalisierung und sogar der Sicherstellung, dass Daten von einem Monat zum nächsten fließen … und das ist, bevor wir überhaupt auf Barrierefreiheit und zusätzliche Layout-Überlegungen eingehen, je nachdem, wo sich der Kalender befindet angezeigt wird und so weiter.

Viele Entwickler fürchten das Date() Objekt und bleibe bei älteren Bibliotheken wie moment.js. Aber während es viele „Fallstricke“ gibt, wenn es um Daten und Formatierung geht, hat JavaScript viele coole APIs und Sachen, die helfen!

Januar 2023 Kalenderraster.
Erstellen von Kalendern mit Blick auf Barrierefreiheit und Internationalisierung

Ich möchte das Rad hier nicht neu erstellen, aber ich werde Ihnen zeigen, wie wir mit Vanille-JavaScript einen verdammt guten Kalender bekommen können. Wir werden nachsehen Zugänglichkeit, mit semantischem Markup und Screenreader-freundlich <time> -tags — sowie Internationalisierung und Formatierung, Verwendung der Intl.Locale, Intl.DateTimeFormat und Intl.NumberFormat-APIs.

Mit anderen Worten, wir erstellen einen Kalender … nur ohne die zusätzlichen Abhängigkeiten, die Sie normalerweise in einem Tutorial wie diesem sehen, und mit einigen der Nuancen, die Sie normalerweise nicht sehen. Und ich hoffe, dass Sie dabei eine neue Wertschätzung für neuere Dinge entwickeln, die JavaScript leisten kann, und gleichzeitig eine Vorstellung von den Dingen bekommen, die mir in den Sinn kommen, wenn ich so etwas zusammenstelle.

Zunächst einmal die Benennung

Wie sollen wir unsere Kalenderkomponente nennen? In meiner Muttersprache würde es „Kalenderelement“ heißen, also verwenden wir das und verkürzen es zu „Kal-El“ – auch bekannt als Supermans Name auf dem Planeten Krypton.

Lassen Sie uns eine Funktion erstellen, um die Dinge in Gang zu bringen:

function kalEl(settings = {}) { ... }

Diese Methode wird gerendert einen einzigen Monat. Später rufen wir diese Methode von auf [...Array(12).keys()] ein ganzes Jahr zu rendern.

Ausgangsdaten und Internationalisierung

Eines der üblichen Dinge, die ein typischer Online-Kalender tut, ist das Hervorheben des aktuellen Datums. Erstellen wir also eine Referenz dafür:

const today = new Date();

Als Nächstes erstellen wir ein „Konfigurationsobjekt“, das wir mit dem optionalen zusammenführen settings Objekt der primären Methode:

const config = Object.assign( { locale: (document.documentElement.getAttribute('lang') || 'en-US'), today: { day: today.getDate(), month: today.getMonth(), year: today.getFullYear() } }, settings
);

Wir prüfen, ob das Wurzelelement (<html>) enthält ein lang-Attribut mit lokal die Info; andernfalls greifen wir auf using zurück en-US. Dies ist der erste Schritt in Richtung Internationalisierung des Kalenders.

Wir müssen auch bestimmen, welcher Monat anfänglich angezeigt werden soll, wenn der Kalender gerendert wird. Deshalb haben wir die verlängert config Objekt mit dem primären date. Auf diese Weise, wenn kein Datum in der angegeben ist settings Objekt verwenden wir die today Verweis stattdessen:

const date = config.date ? new Date(config.date) : today;

Wir benötigen ein wenig mehr Informationen, um den Kalender basierend auf dem Gebietsschema richtig zu formatieren. Beispielsweise wissen wir je nach Gebietsschema möglicherweise nicht, ob der erste Tag der Woche Sonntag oder Montag ist. Wenn wir die Informationen haben, großartig! Aber wenn nicht, aktualisieren wir es mit der Intl.Locale API. Die API hat eine weekInfo Objekt das gibt a zurück firstDay Immobilien, die uns ohne Probleme genau das bietet, wonach wir suchen. Wir können auch erfahren, welche Wochentage dem zugeordnet sind weekend:

if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { firstDay: 7, weekend: [6, 7] };

Auch hier erstellen wir Fallbacks. Der „erste Tag“ der Woche für en-US ist Sonntag, daher ist der Wert standardmäßig auf 7. Das ist etwas verwirrend, da die getDay Methode in JavaScript gibt die Tage als zurück [0-6], Wobei 0 ist Sonntag… frag mich nicht warum. Die Wochenenden sind daher Samstag und Sonntag [6, 7].

Vorher hatten wir die Intl.Locale API und seine weekInfo -Methode war es ziemlich schwierig, einen internationalen Kalender ohne viele **Objekte und Arrays mit Informationen zu jedem Gebietsschema oder jeder Region zu erstellen. Heutzutage ist es kinderleicht. Wenn wir vorbeikommen en-GB, gibt die Methode Folgendes zurück:

// en-GB
{ firstDay: 1, weekend: [6, 7], minimalDays: 4
}

In einem Land wie Brunei (ms-BN), das Wochenende ist Freitag und Sonntag:

// ms-BN
{ firstDay: 7, weekend: [5, 7], minimalDays: 1
}

Sie fragen sich vielleicht, was das ist minimalDays Eigentum ist. Das ist die die wenigsten Tage, die in der ersten Woche eines Monats als volle Woche gezählt werden müssen. In einigen Regionen kann es nur ein Tag sein. Für andere können es volle sieben Tage sein.

Als nächstes erstellen wir eine render Methode innerhalb unserer kalEl-Methode:

const render = (date, locale) => { ... }

Wir brauchen noch einige weitere Daten, mit denen wir arbeiten können, bevor wir etwas rendern:

const month = date.getMonth();
const year = date.getFullYear();
const numOfDays = new Date(year, month + 1, 0).getDate();
const renderToday = (year === config.today.year) && (month === config.today.month);

Der letzte ist ein Boolean das prüft ob today existiert in dem Monat, den wir gerade rendern.

Semantisches Markup

Wir werden uns gleich mit dem Rendering vertiefen. Aber zuerst möchte ich sicherstellen, dass die von uns eingerichteten Details mit semantischen HTML-Tags verknüpft sind. Wenn wir das direkt nach dem Auspacken einrichten, profitieren wir von Anfang an von den Vorteilen der Barrierefreiheit.

Kalenderhülle

Zuerst haben wir den nicht-semantischen Wrapper: <kal-el>. Das ist in Ordnung, weil es keine Semantik gibt <calendar> Etikett oder ähnliches. Wenn wir kein benutzerdefiniertes Element erstellen würden, <article> vielleicht das geeignetste Element, da der Kalender auf einer eigenen Seite stehen könnte.

Monatsnamen

Das <time> element wird für uns eine große Rolle spielen, da es dabei hilft, Datumsangaben in ein Format zu übersetzen, das Screenreader und Suchmaschinen genauer und konsistenter analysieren können. So können wir beispielsweise „Januar 2023“ in unserem Markup vermitteln:

<time datetime="2023-01">January <i>2023</i></time>

Tagesnamen

Die Zeile über den Kalenderdaten, die die Namen der Wochentage enthalten, kann schwierig sein. Es ist ideal, wenn wir die vollständigen Namen für jeden Tag ausschreiben können – z. B. Sonntag, Montag, Dienstag usw. – aber das kann viel Platz beanspruchen. Also kürzen wir die Namen vorerst innerhalb von an ab <ol> wo jeder Tag ein ist <li>:

<ol> <li><abbr title="Sunday">Sun</abbr></li> <li><abbr title="Monday">Mon</abbr></li> <!-- etc. -->
</ol>

Wir könnten mit CSS knifflig werden, um das Beste aus beiden Welten herauszuholen. Zum Beispiel, wenn wir das Markup ein wenig wie folgt ändern:

<ol> <li> <abbr title="S">Sunday</abbr> </li>
</ol>

… erhalten wir standardmäßig die vollständigen Namen. Wir können dann den vollständigen Namen „ausblenden“, wenn der Platz knapp wird, und den anzeigen title Attribut stattdessen:

@media all and (max-width: 800px) { li abbr::after { content: attr(title); }
}

Aber wir gehen diesen Weg nicht, weil die Intl.DateTimeFormat API kann auch hier helfen. Dazu kommen wir im nächsten Abschnitt, wenn wir uns mit dem Rendern befassen.

Tageszahlen

Jedes Datum im Kalenderraster erhält eine Nummer. Jede Nummer ist ein Listenelement (<li>) in einer geordneten Liste (<ol>) und die Inline <time> tag umschließt die eigentliche Zahl.

<li> <time datetime="2023-01-01">1</time>
</li>

Und obwohl ich noch kein Styling vorhabe, weiß ich, dass ich die Datumszahlen irgendwie stylen möchte. Das ist so wie es ist möglich, aber ich möchte auch in der Lage sein, Wochentagsnummern anders als Wochenendnummern zu gestalten, wenn es nötig ist. Also werde ich einschließen data-* Attribute speziell dafür: data-weekend und data-today.

Wochennummern

Es gibt 52 Wochen in einem Jahr, manchmal 53. Obwohl es nicht allzu üblich ist, kann es nett sein, die Zahl für eine bestimmte Woche im Kalender für zusätzlichen Kontext anzuzeigen. Ich mag es jetzt, auch wenn ich es am Ende nicht benutze. Aber wir werden es in diesem Tutorial vollständig verwenden.

Wir verwenden a data-weeknumber -Attribut als Styling-Hook und fügen Sie es in das Markup für jedes Datum ein, das das erste Datum der Woche ist.

<li data-day="7" data-weeknumber="1" data-weekend=""> <time datetime="2023-01-08">8</time>
</li>

Wiedergabe

Lassen Sie uns den Kalender auf eine Seite bringen! Das wissen wir bereits <kal-el> ist der Name unseres benutzerdefinierten Elements. Das erste, was wir konfigurieren müssen, ist die Einstellung der firstDay Eigenschaft darauf, damit der Kalender weiß, ob Sonntag oder ein anderer Tag der erste Tag der Woche ist.

<kal-el data-firstday="${ config.info.firstDay }">

Wir werden verwenden Vorlagenliterale um das Markup zu rendern. Um die Daten für ein internationales Publikum zu formatieren, verwenden wir die Intl.DateTimeFormat API, wieder unter Verwendung der locale wir haben früher angegeben.

Der Monat und das Jahr

Wenn wir anrufen month, können wir einstellen, ob wir die verwenden möchten long Name (z. B. Februar) oder die short Name (z. B. Feb.). Nutzen wir die long Name, da es sich um den Titel über dem Kalender handelt:

<time datetime="${year}-${(pad(month))}"> ${new Intl.DateTimeFormat( locale, { month:'long'}).format(date)} <i>${year}</i>
</time>

Wochentagsnamen

Für Wochentage, die über dem Datumsraster angezeigt werden, benötigen wir sowohl die long (zB „Sonntag“) und short (abgekürzt, dh „Sonne“) Namen. Auf diese Weise können wir den „kurzen“ Namen verwenden, wenn der Platz im Kalender knapp wird:

Intl.DateTimeFormat([locale], { weekday: 'long' })
Intl.DateTimeFormat([locale], { weekday: 'short' })

Lassen Sie uns eine kleine Hilfsmethode erstellen, die es ein wenig einfacher macht, jede einzelne aufzurufen:

const weekdays = (firstDay, locale) => { const date = new Date(0); const arr = [...Array(7).keys()].map(i => { date.setDate(5 + i) return { long: new Intl.DateTimeFormat([locale], { weekday: 'long'}).format(date), short: new Intl.DateTimeFormat([locale], { weekday: 'short'}).format(date) } }) for (let i = 0; i < 8 - firstDay; i++) arr.splice(0, 0, arr.pop()); return arr;
}

So rufen wir das in der Vorlage auf:

<ol> ${weekdays(config.info.firstDay,locale).map(name => ` <li> <abbr title="${name.long}">${name.short}</abbr> </li>`).join('') }
</ol>

Tageszahlen

Und schließlich die Tage, eingehüllt in ein <ol> Element:

${[...Array(numOfDays).keys()].map(i => { const cur = new Date(year, month, i + 1); let day = cur.getDay(); if (day === 0) day = 7; const today = renderToday && (config.today.day === i + 1) ? ' data-today':''; return ` <li data-day="${day}"${today}${i === 0 || day === config.info.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.info.weekend.includes(day) ? ` data-weekend`:''}> <time datetime="${year}-${(pad(month))}-${pad(i)}" tabindex="0"> ${new Intl.NumberFormat(locale).format(i + 1)} </time> </li>`
}).join('')}

Lassen Sie uns das aufschlüsseln:

  1. Wir erstellen ein „Dummy“-Array, basierend auf der „Anzahl der Tage“-Variablen, die wir zum Iterieren verwenden.
  2. Wir schaffen eine day Variable für den aktuellen Tag in der Iteration.
  3. Wir beheben die Diskrepanz zwischen der Intl.Locale API und getDay().
  4. Besitzt das day entspricht today, wir fügen a . hinzu data-* Attribut.
  5. Schließlich geben wir die zurück <li> -Element als Zeichenfolge mit zusammengeführten Daten.
  6. tabindex="0" macht das Element bei Verwendung der Tastaturnavigation nach positiven tabindex-Werten fokussierbar (Hinweis: Sie sollten hört niemals hinzufügen positiv tabindex-Werte)

Zu Zahlen „auffüllen“. der datetime -Attribut verwenden wir eine kleine Hilfsmethode:

const pad = (val) => (val + 1).toString().padStart(2, '0');

Wochennummer

Auch hier ist die „Wochennummer“, wo eine Woche in einem 52-Wochen-Kalender fällt. Auch dafür verwenden wir eine kleine Hilfsmethode:

function getWeek(cur) { const date = new Date(cur.getTime()); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); const week = new Date(date.getFullYear(), 0, 4); return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + (week.getDay() + 6) % 7) / 7);
}

Ich habe das nicht geschrieben getWeek-Methode. Es ist eine aufgeräumte Version von Dieses Skript.

Und das ist es! Danke an die Intl.Locale, Intl.DateTimeFormat und Intl.NumberFormat APIs können wir jetzt einfach ändern lang-Attribut des <html> Element, um den Kontext des Kalenders basierend auf der aktuellen Region zu ändern:

Januar 2023 Kalenderraster.
de-DE
Januar 2023 Kalenderraster.
fa-IR
Januar 2023 Kalenderraster.
zh-Hans-CN-u-nu-hanidec

Kalender gestalten

Sie erinnern sich vielleicht, dass alle Tage nur einer sind <ol> mit Listenelementen. Um diese in einen lesbaren Kalender zu stylen, tauchen wir in die wunderbare Welt von CSS Grid ein. Tatsächlich können wir dasselbe Raster wiederverwenden ein Starter-Kalender-Template gleich hier auf CSS-Tricks, aber ein kleines bisschen mit aktualisiert :is() relationales Pseudo zur Optimierung des Codes.

Beachten Sie, dass ich dabei konfigurierbare CSS-Variablen definiere (und ihnen das Präfix ---kalel- um Konflikte zu vermeiden).

kal-el :is(ol, ul) { display: grid; font-size: var(--kalel-fz, small); grid-row-gap: var(--kalel-row-gap, .33em); grid-template-columns: var(--kalel-gtc, repeat(7, 1fr)); list-style: none; margin: unset; padding: unset; position: relative;
}
Siebenspaltiges Kalenderraster mit angezeigten Rasterlinien.
Erstellen von Kalendern mit Blick auf Barrierefreiheit und Internationalisierung

Lassen Sie uns Rahmen um die Datumszahlen ziehen, um sie visuell voneinander zu trennen:

kal-el :is(ol, ul) li { border-color: var(--kalel-li-bdc, hsl(0, 0%, 80%)); border-style: var(--kalel-li-bds, solid); border-width: var(--kalel-li-bdw, 0 0 1px 0); grid-column: var(--kalel-li-gc, initial); text-align: var(--kalel-li-tal, end); }

Das siebenspaltige Raster funktioniert gut, wenn der erste Tag des Monats ist ebenfalls der erste Tag der Woche für das ausgewählte Gebietsschema). Aber das ist eher die Ausnahme als die Regel. Meistens müssen wir den ersten Tag des Monats auf einen anderen Wochentag verschieben.

Zeigt den ersten Tag des Monats an, der auf einen Donnerstag fällt.
Erstellen von Kalendern mit Blick auf Barrierefreiheit und Internationalisierung

Denken Sie an all das Extra data-* Attribute, die wir beim Schreiben unseres Markups definiert haben? Wir können uns in diese einklinken, um zu aktualisieren, welche Rasterspalte (--kalel-li-gc) wird die erste Datumszahl des Monats gesetzt auf:

[data-firstday="1"] [data-day="3"]:first-child { --kalel-li-gc: 1 / 4;
}

In diesem Fall reichen wir von der ersten Rasterspalte bis zur vierten Rasterspalte – wodurch das nächste Element (Tag 2) automatisch in die fünfte Rasterspalte „geschoben“ wird, und so weiter.

Fügen wir dem „aktuellen“ Datum ein wenig Stil hinzu, damit es auffällt. Das sind nur meine Stile. Sie können hier absolut machen, was Sie wollen.

[data-today] { --kalel-day-bdrs: 50%; --kalel-day-bg: hsl(0, 86%, 40%); --kalel-day-hover-bgc: hsl(0, 86%, 70%); --kalel-day-c: #fff;
}

Ich mag die Idee, die Datumszahlen für Wochenenden anders als für Wochentage zu gestalten. Ich werde eine rötliche Farbe verwenden, um diese zu stylen. Beachten Sie, dass wir für die erreichen können :not() Pseudo-Klasse, um sie auszuwählen, während das aktuelle Datum in Ruhe gelassen wird:

[data-weekend]:not([data-today]) { --kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%));
}

Oh, und vergessen wir nicht die Wochennummern, die vor der ersten Datumsnummer jeder Woche stehen. Wir haben a data-weeknumber Attribut im Markup dafür, aber die Zahlen werden nicht wirklich angezeigt, es sei denn, wir zeigen sie mit CSS an, was wir auf der tun können ::before Pseudoelement:

[data-weeknumber]::before { display: var(--kalel-weeknumber-d, inline-block); content: attr(data-weeknumber); position: absolute; inset-inline-start: 0; /* additional styles */
}

Wir sind an dieser Stelle technisch fertig! Wir können ein Kalenderraster rendern, das die Daten für den aktuellen Monat anzeigt, einschließlich Überlegungen zur Lokalisierung der Daten nach Gebietsschema und um sicherzustellen, dass der Kalender die richtige Semantik verwendet. Und alles, was wir verwendet haben, war Vanille-JavaScript und CSS!

Aber nehmen wir das noch ein Schritt...

Rendering eines ganzen Jahres

Vielleicht müssen Sie ein ganzes Jahr mit Daten anzeigen! Anstatt also den aktuellen Monat zu rendern, möchten Sie vielleicht alle Monatsraster für das aktuelle Jahr anzeigen.

Nun, das Schöne an unserem Ansatz ist, dass wir die aufrufen können render -Methode beliebig oft und ändern lediglich die Ganzzahl, die den Monat in jeder Instanz identifiziert. Nennen wir es 12 Mal basierend auf dem aktuellen Jahr.

so einfach wie das anrufen render-method 12 Mal, und ändern Sie einfach die Ganzzahl für month - i:

[...Array(12).keys()].map(i => render( new Date(date.getFullYear(), i, date.getDate()), config.locale, date.getMonth() )
).join('')

Es ist wahrscheinlich eine gute Idee, einen neuen übergeordneten Wrapper für das gerenderte Jahr zu erstellen. Jedes Kalenderraster ist ein <kal-el> Element. Nennen wir den neuen übergeordneten Wrapper <jor-el>, Wobei Jor-El ist der Name von Kal-Els Vater.

<jor-el id="app" data-year="true"> <kal-el data-firstday="7"> <!-- etc. --> </kal-el> <!-- other months -->
</jor-el>

Wir verwenden <jor-el> um ein Gitter für unsere Gitter zu erstellen. Also Meta!

jor-el { background: var(--jorel-bg, none); display: var(--jorel-d, grid); gap: var(--jorel-gap, 2.5rem); grid-template-columns: var(--jorel-gtc, repeat(auto-fill, minmax(320px, 1fr))); padding: var(--jorel-p, 0);
}

Letzte Demo

Bonus: Konfetti-Kalender

Ich las ein ausgezeichnetes Buch namens Grid machen und brechen neulich und bin über dieses wunderschöne „Neujahrsplakat“ gestolpert:

Erstellen von Kalendern unter Berücksichtigung von Zugänglichkeit und Internationalisierung PlatoBlockchain Data Intelligence. Vertikale Suche. Ai.
Quelle: Das Netz bauen und aufbrechen (2. Auflage) von Timothy Samara

Ich dachte mir, wir könnten etwas Ähnliches machen, ohne irgendetwas in HTML oder JavaScript zu ändern. Ich habe mir die Freiheit genommen, vollständige Namen für Monate und Zahlen anstelle von Tagesnamen einzufügen, um es besser lesbar zu machen. Genießen!

Zeitstempel:

Mehr von CSS-Tricks