Tạo lịch với khả năng truy cập và quốc tế hóa trong tâm trí

Tạo lịch với khả năng truy cập và quốc tế hóa trong tâm trí

Thực hiện tìm kiếm nhanh tại đây trên CSS-Tricks sẽ cho biết có bao nhiêu cách khác nhau để tiếp cận lịch. Một số cho thấy làm thế nào CSS Grid có thể tạo bố cục hiệu quả. Một số nỗ lực để đưa dữ liệu thực tế vào hỗn hợp. Một số dựa vào một khuôn khổ giúp cho công tác quản lý nhà nước.

Có nhiều cân nhắc khi xây dựng một thành phần lịch — nhiều hơn những gì được đề cập trong các bài viết mà tôi đã liên kết. Nếu bạn nghĩ về nó, lịch có rất nhiều sắc thái, từ việc xử lý các múi giờ và định dạng ngày đến bản địa hóa và thậm chí đảm bảo các ngày trôi qua từ tháng này sang tháng khác… và đó là trước khi chúng ta bắt đầu truy cập và xem xét bố cục bổ sung tùy thuộc vào vị trí của lịch được hiển thị và không có gì.

Nhiều nhà phát triển sợ Date() vật và gắn bó với các thư viện cũ hơn như moment.js. Tuy nhiên, trong khi có rất nhiều "vấn đề" khi nói đến ngày tháng và định dạng, thì JavaScript có rất nhiều API và nội dung thú vị để trợ giúp!

Lưới lịch tháng 2023 năm XNUMX.
Tạo lịch với khả năng truy cập và quốc tế hóa trong tâm trí

Tôi không muốn tạo lại bánh xe ở đây, nhưng tôi sẽ chỉ cho bạn cách chúng ta có thể có được một lịch tuyệt vời với vanilla JavaScript. Chúng tôi sẽ xem xét khả năng tiếp cận, sử dụng đánh dấu ngữ nghĩa và thân thiện với trình đọc màn hình <time> -tags - cũng như quốc tế hóađịnh dạng, sử dụng Intl.Locale, Intl.DateTimeFormatIntl.NumberFormat-API.

Nói cách khác, chúng tôi đang tạo lịch… chỉ khi không có các phần phụ thuộc bổ sung mà bạn thường thấy được sử dụng trong hướng dẫn như thế này và với một số sắc thái mà bạn có thể không thường thấy. Và, trong quá trình này, tôi hy vọng bạn sẽ có được sự đánh giá cao hơn đối với những thứ mới hơn mà JavaScript có thể làm trong khi có được ý tưởng về những thứ xuất hiện trong đầu tôi khi tôi kết hợp những thứ như thế này lại với nhau.

Trước hết, đặt tên

Chúng ta nên gọi thành phần lịch của mình là gì? Trong ngôn ngữ mẹ đẻ của tôi, nó sẽ được gọi là "phần tử kalender", vì vậy hãy sử dụng nó và rút ngắn nó thành "Kal-El" - còn được gọi là Tên của Superman trên hành tinh Krypton.

Hãy tạo một chức năng để mọi thứ diễn ra:

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

Phương pháp này sẽ kết xuất một tháng. Sau này chúng ta sẽ gọi phương thức này từ [...Array(12).keys()] để kết xuất cả năm.

Dữ liệu ban đầu và quốc tế hóa

Một trong những điều phổ biến mà lịch trực tuyến điển hình thực hiện là làm nổi bật ngày hiện tại. Vì vậy, hãy tạo một tài liệu tham khảo cho điều đó:

const today = new Date();

Tiếp theo, chúng ta sẽ tạo một “đối tượng cấu hình” mà chúng ta sẽ hợp nhất với đối tượng tùy chọn settings đối tượng của phương thức chính:

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

Chúng tôi kiểm tra, nếu phần tử gốc (<html>) chứa một lang-thuộc tính với miền địa phương thông tin; nếu không, chúng tôi sẽ dự phòng sử dụng en-US. Đây là bước đầu tiên hướng tới quốc tế hóa lịch.

Chúng tôi cũng cần xác định tháng nào sẽ hiển thị ban đầu khi lịch được hiển thị. Đó là lý do tại sao chúng tôi mở rộng config đối tượng với chính date. Bằng cách này, nếu không có ngày nào được cung cấp trong settings đối tượng, chúng tôi sẽ sử dụng today thay vào đó tham chiếu:

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

Chúng tôi cần thêm một chút thông tin để định dạng đúng lịch dựa trên ngôn ngữ. Ví dụ: chúng tôi có thể không biết ngày đầu tuần là Chủ Nhật hay Thứ Hai, tùy thuộc vào địa phương. Nếu chúng ta có thông tin, thật tuyệt! Nhưng nếu không, chúng tôi sẽ cập nhật nó bằng cách sử dụng Intl.Locale API. API có một weekInfo vật điều đó trả về một firstDay tài sản cung cấp cho chúng tôi chính xác những gì chúng tôi đang tìm kiếm mà không gặp bất kỳ rắc rối nào. Chúng tôi cũng có thể nhận được những ngày trong tuần được gán cho weekend:

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

Một lần nữa, chúng tôi tạo ra dự phòng. “Ngày đầu tiên” trong tuần của en-US là Chủ nhật, vì vậy nó mặc định có giá trị là 7. Điều này hơi khó hiểu, vì getDay phương pháp trong JavaScript trả về những ngày như [0-6], Nơi 0 là Chủ nhật… đừng hỏi tôi tại sao. Cuối tuần là thứ bảy và chủ nhật, do đó [6, 7].

Trước khi chúng tôi có Intl.Locale API và nó weekInfo phương pháp, thật khó để tạo lịch quốc tế mà không có nhiều **đối tượng và mảng có thông tin về từng địa phương hoặc khu vực. Ngày nay, thật dễ dàng. Nếu chúng ta vượt qua en-GB, phương thức trả về:

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

Ở một đất nước như Brunei (ms-BN), cuối tuần là thứ sáu và chủ nhật:

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

Bạn có thể tự hỏi đó là gì minimalDays tài sản là. đó là số ngày ít nhất cần thiết trong tuần đầu tiên của một tháng để được tính là một tuần đầy đủ. Ở một số vùng, nó có thể chỉ là một ngày. Đối với những người khác, nó có thể là bảy ngày đầy đủ.

Tiếp theo, chúng ta sẽ tạo một render phương pháp trong của chúng tôi kalEl-phương pháp:

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

Chúng tôi vẫn cần thêm một số dữ liệu để làm việc trước khi kết xuất bất kỳ thứ gì:

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);

Cái cuối cùng là một Boolean cái đó kiểm tra xem today tồn tại trong tháng chúng tôi sắp kết xuất.

Đánh dấu ngữ nghĩa

Chúng ta sẽ tìm hiểu sâu hơn về kết xuất trong giây lát. Nhưng trước tiên, tôi muốn đảm bảo rằng các chi tiết chúng tôi thiết lập có các thẻ HTML ngữ nghĩa được liên kết với chúng. Việc thiết lập tính năng đó ngay từ đầu sẽ mang lại cho chúng tôi các lợi ích về khả năng tiếp cận ngay từ đầu.

trình bao bọc lịch

Đầu tiên, chúng ta có trình bao bọc phi ngữ nghĩa: <kal-el>. Điều đó tốt vì không có ngữ nghĩa <calendar> thẻ hoặc bất cứ điều gì như thế. Nếu chúng tôi không tạo một phần tử tùy chỉnh, <article> có thể là yếu tố thích hợp nhất vì lịch có thể đứng trên trang của chính nó.

tên tháng

Sản phẩm <time> phần tử sẽ trở thành một phần tử quan trọng đối với chúng tôi vì nó giúp dịch ngày tháng sang định dạng mà trình đọc màn hình và công cụ tìm kiếm có thể phân tích cú pháp chính xác và nhất quán hơn. Ví dụ: đây là cách chúng tôi có thể truyền đạt “Tháng 2023 năm XNUMX” trong phần đánh dấu của mình:

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

tên ngày

Hàng phía trên ngày của lịch chứa tên của các ngày trong tuần có thể phức tạp. Thật lý tưởng nếu chúng ta có thể viết tên đầy đủ của mỗi ngày — ví dụ: Chủ nhật, Thứ hai, Thứ ba, v.v. — nhưng điều đó có thể chiếm nhiều chỗ. Vì vậy, bây giờ chúng ta hãy viết tắt các tên bên trong một <ol> nơi mà mỗi ngày là một <li>:

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

Chúng ta có thể gặp khó khăn với CSS để tận dụng tối đa cả hai thế giới. Ví dụ: nếu chúng tôi sửa đổi đánh dấu một chút như thế này:

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

…chúng tôi lấy tên đầy đủ theo mặc định. Sau đó, chúng tôi có thể "ẩn" tên đầy đủ khi hết dung lượng và hiển thị title thay vào đó thuộc tính:

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

Nhưng, chúng tôi sẽ không đi theo con đường đó bởi vì Intl.DateTimeFormat API cũng có thể trợ giúp ở đây. Chúng ta sẽ đề cập đến vấn đề đó trong phần tiếp theo khi đề cập đến kết xuất.

số ngày

Mỗi ngày trong lưới lịch có một số. Mỗi số là một mục danh sách (<li>) trong một danh sách có thứ tự (<ol>) và nội tuyến <time> thẻ bọc số thực tế.

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

Và mặc dù tôi chưa định thực hiện bất kỳ kiểu dáng nào, nhưng tôi biết mình sẽ muốn một số cách để tạo kiểu cho các số ngày. Điều đó là có thể, nhưng tôi cũng muốn có thể định kiểu các số trong tuần khác với các số cuối tuần nếu tôi cần. Vì vậy, tôi sẽ bao gồm data-* thuộc tính cụ thể cho điều đó: data-weekenddata-today.

Số tuần

Có 52 tuần trong một năm, đôi khi là 53. Mặc dù không phải là quá phổ biến, nhưng sẽ rất hay nếu hiển thị số cho một tuần nhất định trong lịch để có thêm ngữ cảnh. Tôi muốn có nó ngay bây giờ, ngay cả khi tôi không muốn sử dụng nó. Nhưng chúng ta sẽ hoàn toàn sử dụng nó trong hướng dẫn này.

Chúng tôi sẽ sử dụng một data-weeknumber thuộc tính dưới dạng móc tạo kiểu và đưa nó vào phần đánh dấu cho mỗi ngày là ngày đầu tiên của tuần.

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

Rendering

Hãy lấy lịch trên một trang! Chúng tôi đã biết rằng <kal-el> là tên của phần tử tùy chỉnh của chúng tôi. Điều đầu tiên chúng ta cần cấu hình nó là thiết lập firstDay trên đó, vì vậy lịch biết Chủ nhật hay một số ngày khác có phải là ngày đầu tuần hay không.

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

Chúng tôi sẽ sử dụng mẫu chữ để hiển thị đánh dấu. Để định dạng ngày cho khán giả quốc tế, chúng tôi sẽ sử dụng Intl.DateTimeFormat API, một lần nữa sử dụng locale chúng tôi đã chỉ định trước đó.

Tháng và năm

Khi chúng tôi gọi month, chúng ta có thể đặt xem chúng ta có muốn sử dụng long tên (ví dụ tháng hai) hoặc short tên (ví dụ: tháng hai). Hãy sử dụng long tên vì nó là tiêu đề phía trên lịch:

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

tên các ngày trong tuần

Đối với các ngày trong tuần được hiển thị phía trên lưới ngày, chúng tôi cần cả hai long (ví dụ: “Chủ nhật”) và short (viết tắt, tức là "Mặt trời") tên. Bằng cách này, chúng ta có thể sử dụng tên “ngắn” khi lịch thiếu chỗ:

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

Hãy tạo một phương thức trợ giúp nhỏ để gọi từng phương thức dễ dàng hơn một chút:

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;
}

Đây là cách chúng tôi gọi nó trong mẫu:

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

số ngày

Và cuối cùng, những ngày, gói gọn trong một <ol> thành phần:

${[...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('')}

Hãy phá vỡ điều đó:

  1. Chúng tôi tạo một mảng "giả", dựa trên biến "số ngày", mà chúng tôi sẽ sử dụng để lặp lại.
  2. Chúng tôi tạo ra một day biến cho ngày hiện tại trong lần lặp.
  3. Chúng tôi khắc phục sự khác biệt giữa Intl.Locale API và getDay().
  4. Nếu day bằng today, chúng tôi thêm một data-* thuộc tính.
  5. Cuối cùng, chúng tôi trả lại <li> phần tử dưới dạng một chuỗi với dữ liệu được hợp nhất.
  6. tabindex="0" làm cho phần tử có thể được đặt tiêu điểm, khi sử dụng điều hướng bàn phím, sau bất kỳ giá trị tabindex dương nào (Lưu ý: bạn nên không bao giờ thêm vào tích cực giá trị tabindex)

Đến "pad" các con số trong datetime thuộc tính, chúng tôi sử dụng một phương thức trợ giúp nhỏ:

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

Số tuần

Một lần nữa, “số tuần” là nơi một tuần rơi vào lịch 52 tuần. Chúng tôi cũng sử dụng một phương thức trợ giúp nhỏ cho điều đó:

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);
}

tôi không viết cái này getWeek-phương pháp. Đó là một phiên bản sạch của kịch bản này.

Và thế là xong! Nhờ sự Intl.Locale, Intl.DateTimeFormatIntl.NumberFormat API, giờ đây chúng ta có thể chỉ cần thay đổi lang-t thuộc tính của <html> yếu tố để thay đổi bối cảnh của lịch dựa trên khu vực hiện tại:

Lưới lịch tháng 2023 năm XNUMX.
de-DE
Lưới lịch tháng 2023 năm XNUMX.
fa-IR
Lưới lịch tháng 2023 năm XNUMX.
zh-Hans-CN-u-nu-hanidec

Tạo kiểu cho lịch

Bạn có thể nhớ rằng tất cả các ngày chỉ là một <ol> với các mục danh sách. Để tạo kiểu cho những thứ này thành một lịch có thể đọc được, chúng ta hãy đi sâu vào thế giới tuyệt vời của CSS Grid. Trên thực tế, chúng ta có thể sử dụng lại cùng một lưới từ một mẫu lịch bắt đầu ngay tại đây trên CSS-Tricks, nhưng đã cập nhật một nụ cười với :is() giả quan hệ để tối ưu hóa mã.

Lưu ý rằng tôi đang xác định các biến CSS có thể định cấu hình trong quá trình thực hiện (và đặt tiền tố cho chúng bằng ---kalel- để tránh xung đột).

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;
}
Lưới lịch bảy cột với các đường lưới được hiển thị.
Tạo lịch với khả năng truy cập và quốc tế hóa trong tâm trí

Hãy vẽ các đường viền xung quanh các số ngày để giúp phân tách chúng một cách trực quan:

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); }

Lưới bảy cột hoạt động tốt khi ngày đầu tiên của tháng là Ngoài ra ngày đầu tuần cho ngôn ngữ đã chọn). Nhưng đó là ngoại lệ hơn là quy tắc. Hầu hết, chúng ta sẽ cần chuyển ngày đầu tiên của tháng sang một ngày trong tuần khác.

Hiển thị ngày đầu tiên của tháng rơi vào thứ Năm.
Tạo lịch với khả năng truy cập và quốc tế hóa trong tâm trí

Ghi nhớ tất cả các bổ sung data-* thuộc tính chúng tôi đã xác định khi viết đánh dấu của mình? Chúng ta có thể nối vào những cái đó để cập nhật cột lưới nào (--kalel-li-gc) số ngày đầu tiên của tháng được đặt trên:

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

Trong trường hợp này, chúng tôi đang kéo dài từ cột lưới đầu tiên đến cột lưới thứ tư — cột này sẽ tự động “đẩy” mục tiếp theo (Ngày 2) sang cột lưới thứ năm, v.v.

Hãy thêm một chút phong cách vào ngày “hiện tại” để nó nổi bật. Đây chỉ là phong cách của tôi. Bạn hoàn toàn có thể làm những gì bạn muốn ở đây.

[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;
}

Tôi thích ý tưởng tạo kiểu số ngày cho các ngày cuối tuần khác với các ngày trong tuần. Tôi sẽ sử dụng màu đỏ để tạo kiểu cho chúng. Lưu ý rằng chúng ta có thể tiếp cận với :not() lớp giả để chọn chúng trong khi để ngày hiện tại một mình:

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

Ồ, và đừng quên các số tuần đi trước số ngày đầu tiên của mỗi tuần. Chúng tôi đã sử dụng một data-weeknumber trong phần đánh dấu cho điều đó, nhưng các con số sẽ không thực sự hiển thị trừ khi chúng tôi hiển thị chúng bằng CSS, điều mà chúng tôi có thể thực hiện trên ::before phần tử giả:

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

Chúng tôi đã hoàn thành về mặt kỹ thuật vào thời điểm này! Chúng tôi có thể hiển thị lưới lịch hiển thị các ngày trong tháng hiện tại, hoàn chỉnh với các cân nhắc để bản địa hóa dữ liệu theo ngôn ngữ và đảm bảo rằng lịch sử dụng đúng ngữ nghĩa. Và tất cả những gì chúng tôi sử dụng là vanilla JavaScript và CSS!

Nhưng hãy lấy cái này thêm một bước...

Kết xuất cả năm

Có thể bạn cần hiển thị đầy đủ các ngày tháng! Vì vậy, thay vì hiển thị tháng hiện tại, bạn có thể muốn hiển thị tất cả lưới tháng cho năm hiện tại.

Chà, điều thú vị về phương pháp chúng tôi đang sử dụng là chúng tôi có thể gọi render phương thức bao nhiêu lần tùy thích và chỉ thay đổi số nguyên xác định tháng trên mỗi trường hợp. Hãy gọi nó là 12 lần dựa trên năm hiện tại.

đơn giản như gọi render-method 12 lần và chỉ cần thay đổi số nguyên cho month - i:

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

Có lẽ nên tạo một trình bao bọc gốc mới cho năm kết xuất. Mỗi lưới lịch là một <kal-el> yếu tố. Hãy gọi trình bao bọc cha mẹ mới <jor-el>, Nơi Jor-El là tên cha của Kal-El.

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

Chúng ta có thể sử dụng <jor-el> để tạo lưới cho lưới của chúng ta. Vì vậy, siêu!

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);
}

Bản trình diễn cuối cùng

Phần thưởng: Lịch Confetti

Tôi đọc một cuốn sách tuyệt vời được gọi là Tạo và phá vỡ lưới vào một ngày khác và tình cờ nhìn thấy “áp phích năm mới” tuyệt đẹp này:

Tạo lịch với khả năng truy cập và quốc tế hóa trong tâm trí PlatoBlockchain Data Intelligence. Tìm kiếm dọc. Ái.
nguồn: Tạo và phá vỡ lưới (Phiên bản 2) của Ti-mô-thê Samara

Tôi hình dung chúng tôi có thể làm điều gì đó tương tự mà không cần thay đổi bất kỳ thứ gì trong HTML hoặc JavaScript. Tôi đã tự do bao gồm tên đầy đủ trong nhiều tháng và số thay vì tên ngày, để làm cho nó dễ đọc hơn. Thưởng thức!

Dấu thời gian:

Thêm từ Thủ thuật CSS