접근성과 국제화를 염두에 두고 캘린더 만들기

접근성과 국제화를 염두에 두고 캘린더 만들기

소스 노드 : 2009236

CSS-Tricks에서 빠른 검색을 수행하면 캘린더에 접근하는 방법이 얼마나 다양한지 알 수 있습니다. 일부는 방법을 보여줍니다. CSS Grid는 레이아웃을 효율적으로 생성할 수 있습니다.. 일부 시도 실제 데이터를 믹스에 가져오기. 약간 틀에 의존하다 상태 관리를 돕기 위해.

캘린더 구성 요소를 구축할 때는 내가 링크한 기사에서 다루는 것보다 훨씬 더 많은 고려 사항이 있습니다. 생각해 보면 캘린더는 시간대 및 날짜 형식 처리에서 현지화에 이르기까지 미묘한 차이로 가득 차 있으며 날짜가 한 달에서 다음 달로 흐르도록 하는 것까지 포함합니다. 등이 표시됩니다.

많은 개발자들이 Date() 대상 다음과 같은 오래된 라이브러리를 고수하십시오. moment.js. 그러나 날짜와 형식에 관해서는 많은 "고장"이 있지만 JavaScript에는 도움이 되는 멋진 API와 기능이 많이 있습니다!

2023년 XNUMX월 달력 그리드.

여기서 바퀴를 다시 만들고 싶지는 않지만 바닐라 JavaScript로 멋진 달력을 얻을 수 있는 방법을 보여 드리겠습니다. 살펴보겠습니다 접근성, 시맨틱 마크업 및 스크린리더 친화적 사용 <time> -태그 —뿐만 아니라 국제화서식사용 Intl.Locale, Intl.DateTimeFormatIntl.NumberFormat-아피스.

즉, 우리는 캘린더를 만들고 있습니다. 이와 같은 자습서에서 일반적으로 사용되는 추가 종속성이 없고 일반적으로 볼 수 없는 약간의 뉘앙스가 있는 경우에만 가능합니다. 그리고 그 과정에서 JavaScript가 할 수 있는 새로운 일에 대한 새로운 인식을 얻으면서 이와 같은 것을 조합할 때 내 마음에 스치는 일에 대한 아이디어를 얻길 바랍니다.

먼저 네이밍

캘린더 구성 요소를 무엇이라고 해야 합니까? 내 모국어로는 "칼렌더 요소"라고 하므로 이를 사용하여 줄여서 "칼-엘"이라고도 합니다. 크립톤 행성의 슈퍼맨 이름.

작업을 진행하는 함수를 만들어 봅시다.

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

이 방법은 한 달. 나중에 이 메소드를 다음에서 호출할 것입니다. [...Array(12).keys()] XNUMX년 전체를 렌더링합니다.

초기 데이터 및 국제화

일반적인 온라인 캘린더가 수행하는 일반적인 작업 중 하나는 현재 날짜를 강조 표시하는 것입니다. 이에 대한 참조를 만들어 보겠습니다.

const today = new Date();

다음으로 선택 항목과 병합할 "구성 개체"를 만듭니다. settings 기본 메서드의 개체:

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

루트 요소(<html>) 포함 lang-속성 장소 정보; 그렇지 않으면 사용으로 대체됩니다. en-US. 향한 첫걸음입니다 달력 국제화.

또한 달력이 렌더링될 때 처음에 표시할 월을 결정해야 합니다. 그래서 우리는 기간을 연장했습니다. config 기본 객체 date. 이렇게 하면 날짜가 입력되지 않은 경우 settings 개체, 우리는 today 대신 참조 :

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

로케일을 기반으로 캘린더의 형식을 올바르게 지정하려면 조금 더 많은 정보가 필요합니다. 예를 들어, 지역에 따라 주의 첫째 날이 일요일인지 월요일인지 모를 수 있습니다. 정보가 있다면 훌륭합니다! 그렇지 않은 경우 다음을 사용하여 업데이트합니다. Intl.Locale API. API에는 weekInfo 대상 그 반환 firstDay 번거로움 없이 우리가 찾고 있는 것을 정확히 제공하는 속성입니다. 우리는 또한 어떤 요일에 할당되는지 알 수 있습니다. weekend:

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

다시, 우리는 폴백을 만듭니다. 한 주의 "첫날" en-US 일요일이므로 기본값은 다음과 같습니다. 7. 이것은 약간 혼란스럽습니다. getDay 방법 JavaScript에서 일을 다음과 같이 반환합니다. [0-6]어디로 0 일요일입니다… 이유는 묻지 마세요. 주말은 토요일과 일요일이므로 [6, 7].

우리가 가지고 있기 전에 Intl.Locale API와 그 weekInfo 각 로케일 또는 지역에 대한 정보가 포함된 **객체 및 배열이 많지 않은 국제 캘린더를 만드는 것은 꽤 어려웠습니다. 요즈음, 그것은 쉬운 완두콩입니다. 우리가 통과하면 en-GB, 메서드는 다음을 반환합니다.

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

브루나이 같은 나라에서 (ms-BN), 주말은 금요일과 일요일입니다.

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

당신은 그것이 무엇인지 궁금해 할 것입니다 minimalDays 재산은. 그게 한 달의 첫 번째 주에 한 주를 계산하는 데 필요한 최소 일수. 일부 지역에서는 단 하루일 수도 있습니다. 다른 사람들에게는 완전한 XNUMX일이 될 수도 있습니다.

다음으로 render 우리의 방법 kalEl-방법:

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

무엇이든 렌더링하기 전에 작업할 데이터가 더 필요합니다.

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

마지막은 Boolean 여부를 확인하는 today 렌더링하려는 달에 존재합니다.

시맨틱 마크 업

잠시 후에 렌더링에 대해 더 깊이 알아볼 것입니다. 그러나 먼저 우리가 설정한 세부 정보에 의미론적 HTML 태그가 연결되어 있는지 확인하고 싶습니다. 즉시 사용할 수 있도록 설정하면 처음부터 접근성 이점을 얻을 수 있습니다.

캘린더 래퍼

먼저 비의미적 래퍼가 있습니다. <kal-el>. 의미가 없기 때문에 괜찮습니다. <calendar> 태그 또는 이와 유사한 것. 맞춤 요소를 만들지 않았다면 <article> 달력이 자체 페이지에 설 수 있기 때문에 가장 적절한 요소일 수 있습니다.

월 이름

XNUMXD덴탈의 <time> 요소는 날짜를 스크린 리더와 검색 엔진이 보다 정확하고 일관되게 구문 분석할 수 있는 형식으로 변환하는 데 도움이 되기 때문에 우리에게 큰 도움이 될 것입니다. 예를 들어 마크업에서 "2023년 XNUMX월"을 전달하는 방법은 다음과 같습니다.

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

요일 이름

요일 이름이 포함된 달력 날짜 위의 행은 까다로울 수 있습니다. 예를 들어 일요일, 월요일, 화요일 등 매일의 전체 이름을 작성할 수 있다면 이상적이지만 많은 공간을 차지할 수 있습니다. 이제 <ol> 매일이 있는 곳 <li>:

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

우리는 두 세계의 장점을 모두 얻기 위해 CSS로 까다로워질 수 있습니다. 예를 들어 마크업을 다음과 같이 약간 수정한 경우:

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

...기본적으로 전체 이름을 얻습니다. 그런 다음 공간이 부족하면 전체 이름을 "숨기고" title 대신 속성:

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

하지만 우리는 그런 식으로 가지 않을 것입니다. Intl.DateTimeFormat 여기에서도 API가 도움이 될 수 있습니다. 렌더링을 다룰 때 다음 섹션에서 이에 대해 다룰 것입니다.

요일 번호

달력 눈금의 각 날짜는 숫자를 가져옵니다. 각 숫자는 목록 항목(<li>) 순서가 지정된 목록(<ol>) 및 인라인 <time> 태그는 실제 숫자를 래핑합니다.

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

그리고 아직 스타일링을 할 계획은 없지만 날짜 숫자의 스타일을 지정하는 방법이 필요하다는 것을 알고 있습니다. 있는 그대로 가능하지만 필요한 경우 주중 숫자와 주말 숫자의 스타일을 다르게 지정할 수 있기를 원합니다. 그래서, 나는 포함 할거야 data-* 속성 구체적으로: data-weekenddata-today.

주 번호

52년에는 53주가 있고 때로는 XNUMX주가 있습니다. 매우 일반적이지는 않지만 추가 컨텍스트를 위해 달력에 주어진 주에 대한 숫자를 표시하는 것이 좋을 수 있습니다. 나는 그것을 사용하지 않게 되더라도 지금 그것을 갖는 것을 좋아합니다. 하지만 이 튜토리얼에서는 완전히 사용할 것입니다.

우리는 사용할 것입니다 data-weeknumber 속성을 스타일링 후크로 지정하고 주의 첫 번째 날짜인 각 날짜의 마크업에 포함합니다.

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

표현

페이지에 달력을 가져오자! 우리는 이미 알고 있습니다 <kal-el> 맞춤 요소의 이름입니다. 먼저 구성해야 할 것은 다음을 설정하는 것입니다. firstDay 따라서 달력은 일요일 또는 다른 날이 주의 첫날인지 알 수 있습니다.

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

우리가 사용할거야. 템플릿 리터럴 마크업을 렌더링합니다. 전 세계 사용자를 위한 날짜 형식을 지정하기 위해 다음을 사용합니다. Intl.DateTimeFormat API, 다시 사용 locale 우리는 이전에 지정했습니다.

월과 년

우리가 부를 때 month, 사용 여부를 설정할 수 있습니다. long 이름(예: XNUMX월) 또는 short 이름(예: XNUMX월). 를 사용하자 long 달력 위의 제목이므로 이름:

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

요일 이름

날짜 그리드 위에 표시되는 평일의 경우 다음 두 가지가 모두 필요합니다. long (예: "일요일") 및 short (약칭, 즉 "Sun") 이름. 이렇게 하면 달력 공간이 부족할 때 "짧은" 이름을 사용할 수 있습니다.

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

각각을 좀 더 쉽게 호출할 수 있는 작은 도우미 메서드를 만들어 보겠습니다.

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

템플릿에서 호출하는 방법은 다음과 같습니다.

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

요일 번호

그리고 마침내, <ol> 요소:

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

이를 분해해 보겠습니다.

  1. 반복에 사용할 "일수" 변수를 기반으로 "더미" 배열을 만듭니다.
  2. 우리는 day 반복의 현재 날짜에 대한 변수입니다.
  3. 우리는 사이의 불일치를 수정 Intl.Locale API와 getDay().
  4. 경우 day 동일하다 today, 우리는 추가 data-* 속성을 사용하지 않는 것입니다.
  5. 마지막으로, 우리는 <li> 병합된 데이터가 있는 문자열로 요소.
  6. tabindex="0" 키보드 탐색을 사용할 때 양수 tabindex 값 다음에 요소를 포커스 가능하게 만듭니다(참고: 더하다 긍정적인 tabindex-값)

숫자 "채우기" FBI 증오 범죄 보고서 datetime 속성에 대해서는 약간의 도우미 메서드를 사용합니다.

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

주 번호

다시 말하지만 "주 번호"는 52주 달력에서 한 주가 속하는 위치입니다. 이를 위해 작은 도우미 메서드도 사용합니다.

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

나는 이것을 쓰지 않았다 getWeek-방법. 정리된 버전입니다 이 스크립트.

그리고 그게 다야! 덕분에 Intl.Locale, Intl.DateTimeFormatIntl.NumberFormat API, 우리는 이제 단순히 lang-의 속성 <html> 현재 지역을 기반으로 캘린더의 컨텍스트를 변경하는 요소:

2023년 XNUMX월 달력 그리드.
de-DE
2023년 XNUMX월 달력 그리드.
fa-IR
2023년 XNUMX월 달력 그리드.
zh-Hans-CN-u-nu-hanidec

캘린더 스타일링

당신은 모든 날이 어떻게 하루인지 기억할 것입니다 <ol> 목록 항목으로. 읽기 쉬운 달력으로 스타일을 지정하기 위해 멋진 CSS Grid 세계로 뛰어들었습니다. 사실, 우리는 동일한 그리드를 바로 여기 CSS-Tricks의 시작 캘린더 템플릿, 그러나 smidge를 다음과 같이 업데이트했습니다. :is() 코드를 최적화하기 위한 관계형 의사.

그 과정에서 구성 가능한 CSS 변수를 정의하고 있습니다. ---kalel- 충돌을 피하기 위해).

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;
}
눈금선이 표시된 XNUMX열 달력 눈금.

시각적으로 구분할 수 있도록 날짜 숫자 주위에 테두리를 그립니다.

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

XNUMX열 그리드는 해당 월의 첫 번째 날이 다음과 같을 때 제대로 작동합니다. 또한 선택한 로케일의 첫 번째 요일). 그러나 그것은 규칙이 아니라 예외입니다. 대부분의 경우 해당 월의 첫 번째 날을 다른 요일로 변경해야 합니다.

목요일에 해당하는 월의 첫 번째 날을 표시합니다.

여분의 모든 것을 기억하십시오 data-* 마크업을 작성할 때 정의한 속성? 어떤 그리드 열(--kalel-li-gc) 해당 월의 첫 번째 날짜 숫자는 다음 위치에 있습니다.

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

이 경우 첫 번째 그리드 열에서 네 번째 그리드 열까지 확장됩니다. 그러면 자동으로 다음 항목(2일차)이 다섯 번째 그리드 열로 "푸시"됩니다.

눈에 잘 띄도록 "현재" 날짜에 약간의 스타일을 추가해 보겠습니다. 이것들은 단지 내 스타일입니다. 여기에서 원하는 것을 완전히 할 수 있습니다.

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

평일과 다르게 주말의 날짜 숫자 스타일을 지정하는 아이디어가 마음에 듭니다. 붉은색을 사용하여 스타일을 지정하겠습니다. 우리는 :not() 현재 날짜를 그대로 두고 선택하는 가상 클래스:

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

아, 그리고 각 주의 첫 번째 날짜 숫자 앞에 오는 주 숫자를 잊지 말자. 우리는 data-weeknumber 하지만 CSS로 공개하지 않는 한 숫자는 실제로 표시되지 않습니다. ::before 유사 요소:

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

이 시점에서 기술적으로 끝났습니다! 현재 월의 날짜를 표시하는 달력 그리드를 렌더링할 수 있으며, 로케일별로 데이터를 지역화하고 달력이 적절한 의미 체계를 사용하는지 확인할 수 있습니다. 그리고 우리가 사용한 것은 바닐라 JavaScript와 CSS뿐이었습니다!

하지만 이것을 가져가자 한 걸음 더...

XNUMX년 내내 렌더링

전체 연도의 날짜를 표시해야 할 수도 있습니다! 따라서 현재 월을 렌더링하는 대신 현재 연도의 모든 월 그리드를 표시할 수 있습니다.

우리가 사용하고 있는 접근 방식의 좋은 점은 render 메서드를 원하는 만큼 여러 번 사용하고 각 인스턴스에서 월을 식별하는 정수를 변경하기만 하면 됩니다. 올해를 기준으로 12번이라고 하자.

전화하는 것만 큼 간단합니다. render-method 12번, 정수만 변경 month - i:

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

렌더링된 연도에 대한 새 부모 래퍼를 만드는 것이 좋습니다. 각 캘린더 그리드는 <kal-el> 요소. 새 상위 래퍼를 호출해 보겠습니다. <jor-el>어디로 Jor-El은 Kal-El의 아버지의 이름입니다..

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

우리는 사용할 수 있습니다 <jor-el> 그리드에 대한 그리드를 생성합니다. 그래서 메타!

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

최종 데모

보너스: 색종이 달력

라는 훌륭한 책을 읽었다. 그리드 만들기 및 깨기 다른 날이 아름다운 "새해 포스터"를 우연히 발견했습니다.

출처: 그리드 만들기 및 깨기(2판) 티모시 사마라

나는 우리가 HTML이나 자바스크립트에서 아무것도 변경하지 않고 비슷한 것을 할 수 있다고 생각했습니다. 더 읽기 쉽도록 달의 전체 이름과 요일 이름 대신 숫자를 자유롭게 포함했습니다. 즐기다!

타임 스탬프 :

더보기 CSS 트릭

천체

소스 노드 : 845518
타임 스탬프 : 2021 년 5 월 5 일