main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

21
node_modules/react-day-picker/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2024 Giampaolo Bellavite <io@gpbl.dev> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

65
node_modules/react-day-picker/README.md generated vendored Normal file
View File

@@ -0,0 +1,65 @@
# React DayPicker
[DayPicker](http://react-day-picker.js.org) is a date picker component for [React](https://reactjs.org). Renders a monthly calendar to select days. DayPicker is customizable, works great with input fields and can be styled to match any design.
➡️ **[react-day-picker.js.org](http://react-day-picker.js.org)** for guides, examples and API reference.
<picture>
<source media="(prefers-color-scheme: dark)" srcSet="https://user-images.githubusercontent.com/120693/188241991-19d0e8a1-230a-48c8-8477-3c90d4e36197.png"/>
<source media="(prefers-color-scheme: light)" srcSet="https://user-images.githubusercontent.com/120693/188238076-311ec6d1-503d-4c21-8ffe-d89faa60e40f.png"/>
<img alt="Shows a screenshot of the React DayPicker component in a browsers window." width="900" />
</picture>
## Main features
- ☀️ Select days, ranges or whatever
- 🧘‍♀️ using [date-fns](http://date-fns.org) as date library
- 🌎 Localizable into any language
- ➡️ Keyboard navigation
- ♿️ [WAI-ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) support
- 🤖 Written in TypeScript
- 🎨 Easy to style and customize
- 🗓 Support multiple calendars
- 📄 Easy to integrate input fields
## Installation
```shell
npm install react-day-picker date-fns # using npm
pnpm install react-day-picker date-fns # using pnpm
yarn add react-day-picker date-fns # using yarn
```
<a href="https://www.npmjs.com/package/react-day-picker"><img src="https://img.shields.io/npm/v/react-day-picker.svg?style=flat-square" alt="npm version"/></a> <img src="https://img.shields.io/npm/dm/react-day-picker.svg?style=flat-square" alt="npm downloads"/> <img src="https://img.shields.io/bundlephobia/minzip/react-day-picker" alt="Min gzipped size"/>
## Example
```tsx
import { useState } from 'react';
import { format } from 'date-fns';
import { DayPicker } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
export default function Example() {
const [selected, setSelected] = useState<Date>();
let footer = <p>Please pick a day.</p>;
if (selected) {
footer = <p>You picked {format(selected, 'PP')}.</p>;
}
return (
<DayPicker
mode="single"
selected={selected}
onSelect={setSelected}
footer={footer}
/>
);
}
```
## Documentation
See **[react-day-picker.js.org](http://react-day-picker.js.org)** for guides, examples and API reference of the latest version.
<small>Docs for version 7 are at <a href="https://react-day-picker-v7.netlify.app" target="_blank">react-day-picker-v7.netlify.app</a>.</small>

1363
node_modules/react-day-picker/dist/index.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

2277
node_modules/react-day-picker/dist/index.esm.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
node_modules/react-day-picker/dist/index.esm.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

2332
node_modules/react-day-picker/dist/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
node_modules/react-day-picker/dist/index.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

2
node_modules/react-day-picker/dist/index.min.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
node_modules/react-day-picker/dist/index.min.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

318
node_modules/react-day-picker/dist/style.css generated vendored Normal file
View File

@@ -0,0 +1,318 @@
.rdp {
--rdp-cell-size: 40px; /* Size of the day cells. */
--rdp-caption-font-size: 18px; /* Font size for the caption labels. */
--rdp-accent-color: #0000ff; /* Accent color for the background of selected days. */
--rdp-background-color: #e7edff; /* Background color for the hovered/focused elements. */
--rdp-accent-color-dark: #3003e1; /* Accent color for the background of selected days (to use in dark-mode). */
--rdp-background-color-dark: #180270; /* Background color for the hovered/focused elements (to use in dark-mode). */
--rdp-outline: 2px solid var(--rdp-accent-color); /* Outline border for focused elements */
--rdp-outline-selected: 3px solid var(--rdp-accent-color); /* Outline border for focused _and_ selected elements */
--rdp-selected-color: #fff; /* Color of selected day text */
margin: 1em;
}
/* Hide elements for devices that are not screen readers */
.rdp-vhidden {
box-sizing: border-box;
padding: 0;
margin: 0;
background: transparent;
border: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
position: absolute !important;
top: 0;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
overflow: hidden !important;
clip: rect(1px, 1px, 1px, 1px) !important;
border: 0 !important;
}
/* Buttons */
.rdp-button_reset {
appearance: none;
position: relative;
margin: 0;
padding: 0;
cursor: default;
color: inherit;
background: none;
font: inherit;
-moz-appearance: none;
-webkit-appearance: none;
}
.rdp-button_reset:focus-visible {
/* Make sure to reset outline only when :focus-visible is supported */
outline: none;
}
.rdp-button {
border: 2px solid transparent;
}
.rdp-button[disabled]:not(.rdp-day_selected) {
opacity: 0.25;
}
.rdp-button:not([disabled]) {
cursor: pointer;
}
.rdp-button:focus-visible:not([disabled]) {
color: inherit;
background-color: var(--rdp-background-color);
border: var(--rdp-outline);
}
.rdp-button:hover:not([disabled]):not(.rdp-day_selected) {
background-color: var(--rdp-background-color);
}
.rdp-months {
display: flex;
}
.rdp-month {
margin: 0 1em;
}
.rdp-month:first-child {
margin-left: 0;
}
.rdp-month:last-child {
margin-right: 0;
}
.rdp-table {
margin: 0;
max-width: calc(var(--rdp-cell-size) * 7);
border-collapse: collapse;
}
.rdp-with_weeknumber .rdp-table {
max-width: calc(var(--rdp-cell-size) * 8);
border-collapse: collapse;
}
.rdp-caption {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
text-align: left;
}
.rdp-multiple_months .rdp-caption {
position: relative;
display: block;
text-align: center;
}
.rdp-caption_dropdowns {
position: relative;
display: inline-flex;
}
.rdp-caption_label {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
margin: 0;
padding: 0 0.25em;
white-space: nowrap;
color: currentColor;
border: 0;
border: 2px solid transparent;
font-family: inherit;
font-size: var(--rdp-caption-font-size);
font-weight: bold;
}
.rdp-nav {
white-space: nowrap;
}
.rdp-multiple_months .rdp-caption_start .rdp-nav {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.rdp-multiple_months .rdp-caption_end .rdp-nav {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
}
.rdp-nav_button {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--rdp-cell-size);
height: var(--rdp-cell-size);
padding: 0.25em;
border-radius: 100%;
}
/* ---------- */
/* Dropdowns */
/* ---------- */
.rdp-dropdown_year,
.rdp-dropdown_month {
position: relative;
display: inline-flex;
align-items: center;
}
.rdp-dropdown {
appearance: none;
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
cursor: inherit;
opacity: 0;
border: none;
background-color: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.rdp-dropdown[disabled] {
opacity: unset;
color: unset;
}
.rdp-dropdown:focus-visible:not([disabled]) + .rdp-caption_label {
background-color: var(--rdp-background-color);
border: var(--rdp-outline);
border-radius: 6px;
}
.rdp-dropdown_icon {
margin: 0 0 0 5px;
}
.rdp-head {
border: 0;
}
.rdp-head_row,
.rdp-row {
height: 100%;
}
.rdp-head_cell {
vertical-align: middle;
font-size: 0.75em;
font-weight: 700;
text-align: center;
height: 100%;
height: var(--rdp-cell-size);
padding: 0;
text-transform: uppercase;
}
.rdp-tbody {
border: 0;
}
.rdp-tfoot {
margin: 0.5em;
}
.rdp-cell {
width: var(--rdp-cell-size);
height: 100%;
height: var(--rdp-cell-size);
padding: 0;
text-align: center;
}
.rdp-weeknumber {
font-size: 0.75em;
}
.rdp-weeknumber,
.rdp-day {
display: flex;
overflow: hidden;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--rdp-cell-size);
max-width: var(--rdp-cell-size);
height: var(--rdp-cell-size);
margin: 0;
border: 2px solid transparent;
border-radius: 100%;
}
.rdp-day_today:not(.rdp-day_outside) {
font-weight: bold;
}
.rdp-day_selected,
.rdp-day_selected:focus-visible,
.rdp-day_selected:hover {
color: var(--rdp-selected-color);
opacity: 1;
background-color: var(--rdp-accent-color);
}
.rdp-day_outside {
opacity: 0.5;
}
.rdp-day_selected:focus-visible {
/* Since the background is the same use again the outline */
outline: var(--rdp-outline);
outline-offset: 2px;
z-index: 1;
}
.rdp:not([dir='rtl']) .rdp-day_range_start:not(.rdp-day_range_end) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.rdp:not([dir='rtl']) .rdp-day_range_end:not(.rdp-day_range_start) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.rdp[dir='rtl'] .rdp-day_range_start:not(.rdp-day_range_end) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.rdp[dir='rtl'] .rdp-day_range_end:not(.rdp-day_range_start) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.rdp-day_range_end.rdp-day_range_start {
border-radius: 100%;
}
.rdp-day_range_middle {
border-radius: 0;
}
/*# sourceMappingURL=style.css.map */

39
node_modules/react-day-picker/dist/style.css.d.ts generated vendored Normal file
View File

@@ -0,0 +1,39 @@
declare const styles: {
'rdp': string
'rdp-vhidden': string
'rdp-button_reset': string
'rdp-button': string
'rdp-day_selected': string
'rdp-months': string
'rdp-month': string
'rdp-table': string
'rdp-with_weeknumber': string
'rdp-caption': string
'rdp-multiple_months': string
'rdp-caption_dropdowns': string
'rdp-caption_label': string
'rdp-nav': string
'rdp-caption_start': string
'rdp-caption_end': string
'rdp-nav_button': string
'rdp-dropdown_year': string
'rdp-dropdown_month': string
'rdp-dropdown': string
'rdp-dropdown_icon': string
'rdp-head': string
'rdp-head_row': string
'rdp-row': string
'rdp-head_cell': string
'rdp-tbody': string
'rdp-tfoot': string
'rdp-cell': string
'rdp-weeknumber': string
'rdp-day': string
'rdp-day_today': string
'rdp-day_outside': string
'rdp-day_range_start': string
'rdp-day_range_end': string
'rdp-day_range_middle': string
}
export default styles

1
node_modules/react-day-picker/dist/style.css.map generated vendored Normal file

File diff suppressed because one or more lines are too long

316
node_modules/react-day-picker/dist/style.module.css generated vendored Normal file
View File

@@ -0,0 +1,316 @@
.root {
--rdp-cell-size: 40px; /* Size of the day cells. */
--rdp-caption-font-size: 18px; /* Font size for the caption labels. */
--rdp-accent-color: #0000ff; /* Accent color for the background of selected days. */
--rdp-background-color: #e7edff; /* Background color for the hovered/focused elements. */
--rdp-accent-color-dark: #3003e1; /* Accent color for the background of selected days (to use in dark-mode). */
--rdp-background-color-dark: #180270; /* Background color for the hovered/focused elements (to use in dark-mode). */
--rdp-outline: 2px solid var(--rdp-accent-color); /* Outline border for focused elements */
--rdp-outline-selected: 3px solid var(--rdp-accent-color); /* Outline border for focused _and_ selected elements */
--rdp-selected-color: #fff; /* Color of selected day text */
margin: 1em;
}
/* Hide elements for devices that are not screen readers */
.vhidden {
box-sizing: border-box;
padding: 0;
margin: 0;
background: transparent;
border: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
position: absolute !important;
top: 0;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
overflow: hidden !important;
clip: rect(1px, 1px, 1px, 1px) !important;
border: 0 !important;
}
/* Buttons */
.button_reset {
appearance: none;
position: relative;
margin: 0;
padding: 0;
cursor: default;
color: inherit;
background: none;
font: inherit;
-moz-appearance: none;
-webkit-appearance: none;
}
.button_reset:focus-visible {
/* Make sure to reset outline only when :focus-visible is supported */
outline: none;
}
.button {
border: 2px solid transparent;
}
.button[disabled]:not(.day_selected) {
opacity: 0.25;
}
.button:not([disabled]) {
cursor: pointer;
}
.button:focus-visible:not([disabled]) {
color: inherit;
background-color: var(--rdp-background-color);
border: var(--rdp-outline);
}
.button:hover:not([disabled]):not(.day_selected) {
background-color: var(--rdp-background-color);
}
.months {
display: flex;
}
.month {
margin: 0 1em;
}
.month:first-child {
margin-left: 0;
}
.month:last-child {
margin-right: 0;
}
.table {
margin: 0;
max-width: calc(var(--rdp-cell-size) * 7);
border-collapse: collapse;
}
.with_weeknumber .table {
max-width: calc(var(--rdp-cell-size) * 8);
border-collapse: collapse;
}
.caption {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
text-align: left;
}
.multiple_months .caption {
position: relative;
display: block;
text-align: center;
}
.caption_dropdowns {
position: relative;
display: inline-flex;
}
.caption_label {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
margin: 0;
padding: 0 0.25em;
white-space: nowrap;
color: currentColor;
border: 0;
border: 2px solid transparent;
font-family: inherit;
font-size: var(--rdp-caption-font-size);
font-weight: bold;
}
.nav {
white-space: nowrap;
}
.multiple_months .caption_start .nav {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.multiple_months .caption_end .nav {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
}
.nav_button {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--rdp-cell-size);
height: var(--rdp-cell-size);
padding: 0.25em;
border-radius: 100%;
}
/* ---------- */
/* Dropdowns */
/* ---------- */
.dropdown_year,
.dropdown_month {
position: relative;
display: inline-flex;
align-items: center;
}
.dropdown {
appearance: none;
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
cursor: inherit;
opacity: 0;
border: none;
background-color: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.dropdown[disabled] {
opacity: unset;
color: unset;
}
.dropdown:focus-visible:not([disabled]) + .caption_label {
background-color: var(--rdp-background-color);
border: var(--rdp-outline);
border-radius: 6px;
}
.dropdown_icon {
margin: 0 0 0 5px;
}
.head {
border: 0;
}
.head_row,
.row {
height: 100%;
}
.head_cell {
vertical-align: middle;
font-size: 0.75em;
font-weight: 700;
text-align: center;
height: 100%;
height: var(--rdp-cell-size);
padding: 0;
text-transform: uppercase;
}
.tbody {
border: 0;
}
.tfoot {
margin: 0.5em;
}
.cell {
width: var(--rdp-cell-size);
height: 100%;
height: var(--rdp-cell-size);
padding: 0;
text-align: center;
}
.weeknumber {
font-size: 0.75em;
}
.weeknumber,
.day {
display: flex;
overflow: hidden;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--rdp-cell-size);
max-width: var(--rdp-cell-size);
height: var(--rdp-cell-size);
margin: 0;
border: 2px solid transparent;
border-radius: 100%;
}
.day_today:not(.day_outside) {
font-weight: bold;
}
.day_selected,
.day_selected:focus-visible,
.day_selected:hover {
color: var(--rdp-selected-color);
opacity: 1;
background-color: var(--rdp-accent-color);
}
.day_outside {
opacity: 0.5;
}
.day_selected:focus-visible {
/* Since the background is the same use again the outline */
outline: var(--rdp-outline);
outline-offset: 2px;
z-index: 1;
}
.root:not([dir='rtl']) .day_range_start:not(.day_range_end) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.root:not([dir='rtl']) .day_range_end:not(.day_range_start) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.root[dir='rtl'] .day_range_start:not(.day_range_end) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.root[dir='rtl'] .day_range_end:not(.day_range_start) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.day_range_end.day_range_start {
border-radius: 100%;
}
.day_range_middle {
border-radius: 0;
}

View File

@@ -0,0 +1,39 @@
declare const styles: {
'root': string
'vhidden': string
'button_reset': string
'button': string
'day_selected': string
'months': string
'month': string
'table': string
'with_weeknumber': string
'caption': string
'multiple_months': string
'caption_dropdowns': string
'caption_label': string
'nav': string
'caption_start': string
'caption_end': string
'nav_button': string
'dropdown_year': string
'dropdown_month': string
'dropdown': string
'dropdown_icon': string
'head': string
'head_row': string
'row': string
'head_cell': string
'tbody': string
'tfoot': string
'cell': string
'weeknumber': string
'day': string
'day_today': string
'day_outside': string
'day_range_start': string
'day_range_end': string
'day_range_middle': string
}
export default styles

90
node_modules/react-day-picker/package.json generated vendored Normal file
View File

@@ -0,0 +1,90 @@
{
"name": "react-day-picker",
"version": "8.10.1",
"description": "Customizable Date Picker for React",
"author": "Giampaolo Bellavite <io@gpbl.dev>",
"homepage": "http://react-day-picker.js.org",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/gpbl/react-day-picker"
},
"bugs": {
"url": "https://github.com/gpbl/react-day-picker/issues"
},
"main": "dist/index.js",
"module": "dist/index.esm.js",
"unpkg": "dist/index.min.js",
"types": "dist/index.d.ts",
"style": "dist/style.css",
"scripts": {
"prepublish": "pnpm build",
"build": "rimraf dist && rollup -c",
"build-watch": "rollup -c -w",
"lint": "eslint .",
"test": "jest",
"test-watch": "jest --watch",
"typecheck": "tsc --project ./tsconfig.json --noEmit",
"typecheck-watch": "tsc --project ./tsconfig.json --noEmit --watch"
},
"files": [
"dist",
"src",
"tsconfig.json"
],
"devDependencies": {
"@jest/types": "^29.6.3",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.1",
"@rollup/plugin-typescript": "^11.1.5",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/testing-library__jest-dom": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"date-fns": "^3.0.6",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^6.2.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"mockdate": "^3.0.5",
"postcss": "^8.4.32",
"postcss-d-ts": "^1.2.0",
"postcss-typescript-d-ts": "^1.0.0",
"prettier": "^3.1.1",
"react": "^18.2.0",
"react-classnaming": "^0.16.4",
"react-dom": "^18.2.0",
"rimraf": "^5.0.1",
"rollup": "^4.9.1",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-postcss": "^4.0.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.8",
"tslib": "^2.6.2",
"typescript": "~5.3.3"
},
"peerDependencies": {
"date-fns": "^2.28.0 || ^3.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
}
}

1
node_modules/react-day-picker/src/.eslintignore generated vendored Normal file
View File

@@ -0,0 +1 @@
style.css.d.ts

114
node_modules/react-day-picker/src/DayPicker.tsx generated vendored Normal file
View File

@@ -0,0 +1,114 @@
import { DayPickerDefaultProps } from 'types/DayPickerDefault';
import { DayPickerMultipleProps } from 'types/DayPickerMultiple';
import { DayPickerRangeProps } from 'types/DayPickerRange';
import { DayPickerSingleProps } from 'types/DayPickerSingle';
import { Root } from './components/Root';
import { RootProvider } from './contexts/RootProvider';
export type DayPickerProps =
| DayPickerDefaultProps
| DayPickerSingleProps
| DayPickerMultipleProps
| DayPickerRangeProps;
/**
* DayPicker render a date picker component to let users pick dates from a
* calendar. See http://react-day-picker.js.org for updated documentation and
* examples.
*
* ### Customization
*
* DayPicker offers different customization props. For example,
*
* - show multiple months using `numberOfMonths`
* - display a dropdown to navigate the months via `captionLayout`
* - display the week numbers with `showWeekNumbers`
* - disable or hide days with `disabled` or `hidden`
*
* ### Controlling the months
*
* Change the initially displayed month using the `defaultMonth` prop. The
* displayed months are controlled by DayPicker and stored in its internal
* state. To control the months yourself, use `month` instead of `defaultMonth`
* and use the `onMonthChange` event to set it.
*
* To limit the months the user can navigate to, use
* `fromDate`/`fromMonth`/`fromYear` or `toDate`/`toMonth`/`toYear`.
*
* ### Selection modes
*
* DayPicker supports different selection mode that can be toggled using the
* `mode` prop:
*
* - `mode="single"`: only one day can be selected. Use `required` to make the
* selection required. Use the `onSelect` event handler to get the selected
* days.
* - `mode="multiple"`: users can select one or more days. Limit the amount of
* days that can be selected with the `min` or the `max` props.
* - `mode="range"`: users can select a range of days. Limit the amount of days
* in the range with the `min` or the `max` props.
* - `mode="default"` (default): the built-in selections are disabled. Implement
* your own selection mode with `onDayClick`.
*
* The selection modes should cover the most common use cases. In case you
* need a more refined way of selecting days, use `mode="default"`. Use the
* `selected` props and add the day event handlers to add/remove days from the
* selection.
*
* ### Modifiers
*
* A _modifier_ represents different styles or states for the days displayed in
* the calendar (like "selected" or "disabled"). Define custom modifiers using
* the `modifiers` prop.
*
* ### Formatters and custom component
*
* You can customize how the content is displayed in the date picker by using
* either the formatters or replacing the internal components.
*
* For the most common cases you want to use the `formatters` prop to change how
* the content is formatted in the calendar. Use the `components` prop to
* replace the internal components, like the navigation icons.
*
* ### Styling
*
* DayPicker comes with a default, basic style in `react-day-picker/style` use
* it as template for your own style.
*
* If you are using CSS modules, pass the imported styles object the
* `classNames` props.
*
* You can also style the elements via inline styles using the `styles` prop.
*
* ### Form fields
*
* If you need to bind the date picker to a form field, you can use the
* `useInput` hooks for a basic behavior. See the `useInput` source as an
* example to bind the date picker with form fields.
*
* ### Localization
*
* To localize DayPicker, import the locale from `date-fns` package and use the
* `locale` prop.
*
* For example, to use Spanish locale:
*
* ```
* import { es } from 'date-fns/locale';
* <DayPicker locale={es} />
* ```
*/
export function DayPicker(
props:
| DayPickerDefaultProps
| DayPickerSingleProps
| DayPickerMultipleProps
| DayPickerRangeProps
): JSX.Element {
return (
<RootProvider {...props}>
<Root initialProps={props} />
</RootProvider>
);
}

View File

@@ -0,0 +1,45 @@
import { screen } from '@testing-library/react';
import { customRender } from 'test/render';
import { Button } from './Button';
let button: HTMLButtonElement;
describe('when rendered without props', () => {
beforeEach(() => {
customRender(<Button className="foo" style={{ color: 'blue' }} />);
button = screen.getByRole('button');
});
test('should render a button with type "button"', () => {
expect(button).toHaveAttribute('type', 'button');
});
test('should render a button with the button class name', () => {
expect(button).toHaveClass('rdp-button');
});
test('should render a button with the reset class name', () => {
expect(button).toHaveClass('rdp-button_reset');
});
test('should add the class name', () => {
expect(button).toHaveClass('foo');
});
test('should apply the style', () => {
expect(button).toHaveStyle({ color: 'blue' });
});
});
describe('when using class names and styles from context', () => {
beforeEach(() => {
customRender(<Button />, {
classNames: { button: 'foo' },
styles: { button: { color: 'red' } }
});
button = screen.getByRole('button');
});
test('should apply the style', () => {
expect(button).toHaveStyle({ color: 'red' });
});
test('should apply the class name', () => {
expect(button).toHaveClass('foo');
});
});

View File

@@ -0,0 +1,34 @@
import { forwardRef } from 'react';
import { useDayPicker } from 'contexts/DayPicker';
/** The props for the {@link Button} component. */
export type ButtonProps = JSX.IntrinsicElements['button'];
/** Render a button HTML element applying the reset class name. */
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { classNames, styles } = useDayPicker();
const classNamesArr = [classNames.button_reset, classNames.button];
if (props.className) {
classNamesArr.push(props.className);
}
const className = classNamesArr.join(' ');
const style = { ...styles.button_reset, ...styles.button };
if (props.style) {
Object.assign(style, props.style);
}
return (
<button
{...props}
ref={ref}
type="button"
className={className}
style={style}
/>
);
}
);

View File

@@ -0,0 +1 @@
export * from './Button';

View File

@@ -0,0 +1,108 @@
import { screen } from '@testing-library/react';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import {
getMonthCaption,
getMonthDropdown,
getNextButton,
getPrevButton,
getYearDropdown,
queryNextButton,
queryPrevButton
} from 'test/selectors';
import { freezeBeforeAll } from 'test/utils';
import { CustomComponents } from 'types/DayPickerBase';
import { Caption, CaptionProps } from './Caption';
const today = new Date(2021, 8);
freezeBeforeAll(today);
function setup(props: CaptionProps, dayPickerProps?: DayPickerProps) {
customRender(<Caption {...props} />, dayPickerProps);
}
describe('when navigation is disabled', () => {
const props = { displayMonth: today };
const dayPickerProps = { disableNavigation: true };
beforeEach(() => setup(props, dayPickerProps));
test('should display the caption label', () => {
expect(getMonthCaption()).toHaveTextContent('September 2021');
});
test('should not render the navigation', () => {
expect(queryPrevButton()).toBeNull();
expect(queryNextButton()).toBeNull();
});
});
describe('when using a custom CaptionLabel component', () => {
const components: CustomComponents = {
CaptionLabel: () => <>custom label foo</>
};
const props = { displayMonth: today };
beforeEach(() => {
setup(props, { components });
});
test('it should render the custom component instead', () => {
expect(screen.getByText('custom label foo')).toBeInTheDocument();
});
});
describe('when the caption layout is "dropdown"', () => {
const dayPickerProps: DayPickerProps = {
captionLayout: 'dropdown',
fromYear: 2020,
toYear: 2025
};
const props = { displayMonth: today };
beforeEach(() => {
setup(props, dayPickerProps);
});
test('should render the month drop-down', () => {
expect(getMonthDropdown()).toBeInTheDocument();
});
test('should render the year drop-down', () => {
expect(getYearDropdown()).toBeInTheDocument();
});
});
describe('when the caption layout is "buttons"', () => {
const dayPickerProps: DayPickerProps = {
captionLayout: 'buttons'
};
test('should render the next month button', () => {
customRender(<Caption displayMonth={today} />, dayPickerProps);
expect(getNextButton()).toBeInTheDocument();
});
test('should render the previous month button', () => {
customRender(<Caption displayMonth={today} />, dayPickerProps);
expect(getPrevButton()).toBeInTheDocument();
});
});
describe('when the caption layout is "dropdown-buttons"', () => {
const dayPickerProps: DayPickerProps = {
captionLayout: 'dropdown-buttons',
fromYear: 2020,
toYear: 2025
};
const props = { displayMonth: today };
beforeEach(() => {
setup(props, dayPickerProps);
});
test('should render the month drop-down', () => {
expect(getMonthDropdown()).toBeInTheDocument();
});
test('should render the year drop-down', () => {
expect(getYearDropdown()).toBeInTheDocument();
});
test('should render the next month button', () => {
expect(getNextButton()).toBeInTheDocument();
});
test('should render the previous month button', () => {
expect(getPrevButton()).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,77 @@
import { CaptionDropdowns } from 'components/CaptionDropdowns';
import { CaptionLabel } from 'components/CaptionLabel';
import { CaptionNavigation } from 'components/CaptionNavigation';
import { useDayPicker } from 'contexts/DayPicker';
/** Represent the props of the {@link Caption} component. */
export interface CaptionProps {
/** The ID for the heading element. Must be the same as the labelled-by in Table. */
id?: string;
/** The month where the caption is displayed. */
displayMonth: Date;
/** The index of the month where the caption is displayed. Older custom components may miss this prop. */
displayIndex?: number | undefined;
}
/**
* The layout of the caption:
*
* - `dropdown`: display dropdowns for choosing the month and the year.
* - `buttons`: display previous month / next month buttons.
* - `dropdown-buttons`: display both month / year dropdowns and previous month / next month buttons.
*/
export type CaptionLayout = 'dropdown' | 'buttons' | 'dropdown-buttons';
/**
* Render the caption of a month. The caption has a different layout when
* setting the {@link DayPickerBase.captionLayout} prop.
*/
export function Caption(props: CaptionProps): JSX.Element {
const { classNames, disableNavigation, styles, captionLayout, components } =
useDayPicker();
const CaptionLabelComponent = components?.CaptionLabel ?? CaptionLabel;
let caption: JSX.Element;
if (disableNavigation) {
caption = (
<CaptionLabelComponent id={props.id} displayMonth={props.displayMonth} />
);
} else if (captionLayout === 'dropdown') {
caption = (
<CaptionDropdowns displayMonth={props.displayMonth} id={props.id} />
);
} else if (captionLayout === 'dropdown-buttons') {
caption = (
<>
<CaptionDropdowns
displayMonth={props.displayMonth}
displayIndex={props.displayIndex}
id={props.id}
/>
<CaptionNavigation
displayMonth={props.displayMonth}
displayIndex={props.displayIndex}
id={props.id}
/>
</>
);
} else {
caption = (
<>
<CaptionLabelComponent
id={props.id}
displayMonth={props.displayMonth}
displayIndex={props.displayIndex}
/>
<CaptionNavigation displayMonth={props.displayMonth} id={props.id} />
</>
);
}
return (
<div className={classNames.caption} style={styles.caption}>
{caption}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Caption';

View File

@@ -0,0 +1,121 @@
import { screen } from '@testing-library/react';
import { setMonth, setYear } from 'date-fns';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import {
getMonthDropdown,
getYearDropdown,
queryMonthDropdown,
queryYearDropdown
} from 'test/selectors';
import { user } from 'test/user';
import { freezeBeforeAll } from 'test/utils';
import { CaptionProps } from 'components/Caption';
import { CustomComponents } from 'types/DayPickerBase';
import { CaptionDropdowns } from './CaptionDropdowns';
const today = new Date(2021, 8);
const fromYear = 2020;
const toYear = 2025;
freezeBeforeAll(today);
function setup(props: CaptionProps, dayPickerProps?: DayPickerProps) {
customRender(<CaptionDropdowns {...props} />, dayPickerProps);
}
describe('when using a custom CaptionLabel component', () => {
const components: CustomComponents = {
CaptionLabel: () => <>custom label foo</>
};
const props = { displayMonth: today };
beforeEach(() => {
setup(props, { components });
});
test('it should render the custom component instead', () => {
expect(screen.getByText('custom label foo')).toBeInTheDocument();
});
});
describe('when rendered with custom styles or classnames', () => {
let container: HTMLElement;
beforeEach(() => {
const dayPickerProps: DayPickerProps = {
captionLayout: 'dropdown',
fromYear,
toYear,
classNames: { caption_dropdowns: 'foo_dropdowns' },
styles: { caption_dropdowns: { color: 'red' } }
};
const view = customRender(
<CaptionDropdowns displayMonth={today} />,
dayPickerProps
);
container = view.container;
});
test('should use the `caption_dropdowns` class name', () => {
expect(container.firstChild).toHaveClass('foo_dropdowns');
});
test('should use the `caption_dropdowns` style', () => {
expect(container.firstChild).toHaveStyle({ color: 'red' });
});
test('should render the month drop-down', () => {
expect(getMonthDropdown()).toBeInTheDocument();
});
test('should render the year drop-down', () => {
expect(getYearDropdown()).toBeInTheDocument();
});
});
describe('when a month is selected', () => {
const dayPickerProps: DayPickerProps = {
captionLayout: 'dropdown',
fromYear,
toYear,
onMonthChange: jest.fn()
};
beforeEach(() => {
customRender(<CaptionDropdowns displayMonth={today} />, dayPickerProps);
});
describe('from the months drop-down', () => {
const newMonth = setMonth(today, 0);
beforeEach(async () => {
await user.selectOptions(
getMonthDropdown(),
newMonth.getMonth().toString()
);
});
test('should call the `onMonthChange` callback', () => {
expect(dayPickerProps.onMonthChange).toHaveBeenCalledWith(newMonth);
});
});
describe('from the years drop-down', () => {
const newYear = setYear(today, 2022);
beforeEach(async () => {
await user.selectOptions(
getYearDropdown(),
newYear.getFullYear().toString()
);
});
test('should call the `onMonthChange` callback', () => {
expect(dayPickerProps.onMonthChange).toHaveBeenCalledWith(newYear);
});
});
});
describe('when no date limits are set', () => {
const dayPickerProps: DayPickerProps = {
captionLayout: 'dropdown'
};
beforeEach(() => {
customRender(<CaptionDropdowns displayMonth={today} />, dayPickerProps);
});
test('should not render the drop-downs', () => {
expect(queryMonthDropdown()).toBeNull();
expect(queryYearDropdown()).toBeNull();
});
});

View File

@@ -0,0 +1,44 @@
import { addMonths } from 'date-fns';
import { CaptionProps } from 'components/Caption/Caption';
import { CaptionLabel } from 'components/CaptionLabel';
import { MonthsDropdown } from 'components/MonthsDropdown';
import { YearsDropdown } from 'components/YearsDropdown';
import { useDayPicker } from 'contexts/DayPicker';
import { useNavigation } from 'contexts/Navigation';
import { MonthChangeEventHandler } from 'types/EventHandlers';
/**
* Render a caption with the dropdowns to navigate between months and years.
*/
export function CaptionDropdowns(props: CaptionProps): JSX.Element {
const { classNames, styles, components } = useDayPicker();
const { goToMonth } = useNavigation();
const handleMonthChange: MonthChangeEventHandler = (newMonth) => {
goToMonth(
addMonths(newMonth, props.displayIndex ? -props.displayIndex : 0)
);
};
const CaptionLabelComponent = components?.CaptionLabel ?? CaptionLabel;
const captionLabel = (
<CaptionLabelComponent id={props.id} displayMonth={props.displayMonth} />
);
return (
<div
className={classNames.caption_dropdowns}
style={styles.caption_dropdowns}
>
{/* Caption label is visually hidden but for a11y. */}
<div className={classNames.vhidden}>{captionLabel}</div>
<MonthsDropdown
onChange={handleMonthChange}
displayMonth={props.displayMonth}
/>
<YearsDropdown
onChange={handleMonthChange}
displayMonth={props.displayMonth}
/>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './CaptionDropdowns';

View File

@@ -0,0 +1,27 @@
import { customRender } from 'test/render';
import { getMonthCaption } from 'test/selectors';
import { freezeBeforeAll } from 'test/utils';
import { CaptionLabel } from './CaptionLabel';
const today = new Date(1979, 8);
freezeBeforeAll(today);
test('should render the formatted display month', () => {
customRender(<CaptionLabel displayMonth={today} />);
expect(getMonthCaption()).toHaveTextContent('September 1979');
});
test('should apply the `caption_label` class name', () => {
customRender(<CaptionLabel displayMonth={today} />, {
classNames: { caption_label: 'foo' }
});
expect(getMonthCaption()).toHaveClass('foo');
});
test('should apply the `caption_label` style', () => {
customRender(<CaptionLabel displayMonth={today} />, {
styles: { caption_label: { color: 'red' } }
});
expect(getMonthCaption()).toHaveStyle({ color: 'red' });
});

View File

@@ -0,0 +1,32 @@
import { useDayPicker } from 'contexts/DayPicker';
/** The props for the {@link CaptionLabel} component. */
export interface CaptionLabelProps {
/** The ID for the heading element. Must be the same as the labelled-by in Table. */
id?: string;
/** The month where the caption is displayed. */
displayMonth: Date;
/** The index of the month where the caption is displayed. Older custom components may miss this prop. */
displayIndex?: number | undefined;
}
/** Render the caption for the displayed month. This component is used when `captionLayout="buttons"`. */
export function CaptionLabel(props: CaptionLabelProps): JSX.Element {
const {
locale,
classNames,
styles,
formatters: { formatCaption }
} = useDayPicker();
return (
<div
className={classNames.caption_label}
style={styles.caption_label}
aria-live="polite"
role="presentation"
id={props.id}
>
{formatCaption(props.displayMonth, { locale })}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './CaptionLabel';

View File

@@ -0,0 +1,144 @@
import { addMonths } from 'date-fns';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import {
getNextButton,
getPrevButton,
queryNextButton,
queryPrevButton
} from 'test/selectors';
import { user } from 'test/user';
import { freezeBeforeAll } from 'test/utils';
import { CaptionNavigation } from './CaptionNavigation';
const today = new Date(2021, 8);
freezeBeforeAll(today);
describe('when rendered', () => {
const dayPickerProps: DayPickerProps = {
captionLayout: 'buttons'
};
test('should render the next month button', () => {
customRender(<CaptionNavigation displayMonth={today} />, dayPickerProps);
expect(getNextButton()).toBeInTheDocument();
});
test('should render the previous month button', () => {
customRender(<CaptionNavigation displayMonth={today} />, dayPickerProps);
expect(getPrevButton()).toBeInTheDocument();
});
describe('when displaying the first of multiple months', () => {
const numberOfMonths = 3;
beforeEach(() => {
customRender(<CaptionNavigation displayMonth={today} />, {
...dayPickerProps,
numberOfMonths
});
});
test('should not display the next month button', () => {
expect(queryNextButton()).toBeNull();
});
test('should show the previous month button', () => {
expect(getPrevButton()).toBeInTheDocument();
});
});
describe('when displaying the last of multiple months', () => {
const numberOfMonths = 3;
beforeEach(() => {
const lastMonth = addMonths(today, numberOfMonths - 1);
customRender(<CaptionNavigation displayMonth={lastMonth} />, {
...dayPickerProps,
numberOfMonths
});
});
test('should hide the previous month button', () => {
expect(queryPrevButton()).toBeNull();
});
test('should show the next month button', () => {
expect(getNextButton()).toBeInTheDocument();
});
});
describe('when displaying a month in the middle of multiple months', () => {
const numberOfMonths = 3;
beforeEach(() => {
const lastMonth = addMonths(today, numberOfMonths - 2);
customRender(<CaptionNavigation displayMonth={lastMonth} />, {
...dayPickerProps,
numberOfMonths
});
});
test('should not render the previous month button', () => {
expect(queryPrevButton()).toBeNull();
});
test('should not render the next month button', () => {
expect(queryNextButton()).toBeNull();
});
});
describe('when clicking the previous button', () => {
describe('and a previous month is defined', () => {
const testContext = {
...dayPickerProps,
onMonthChange: jest.fn()
};
const previousMonth = addMonths(today, -1);
beforeEach(async () => {
customRender(<CaptionNavigation displayMonth={today} />, testContext);
await user.click(getPrevButton());
});
test('should call the `onMonthChange` callback', () => {
expect(testContext.onMonthChange).toHaveBeenCalledWith(previousMonth);
});
});
describe('and the previous month is not defined', () => {
const testContext = {
...dayPickerProps,
fromDate: today,
onMonthChange: jest.fn()
};
beforeEach(async () => {
customRender(<CaptionNavigation displayMonth={today} />, testContext);
await user.click(getPrevButton());
});
test('should call the `onMonthChange` callback', () => {
expect(testContext.onMonthChange).not.toHaveBeenCalled();
});
});
});
describe('when clicking the next month button', () => {
describe('and the next month is defined', () => {
const testContext = {
...dayPickerProps,
onMonthChange: jest.fn()
};
const nextMonth = addMonths(today, 1);
beforeEach(async () => {
customRender(<CaptionNavigation displayMonth={today} />, testContext);
await user.click(getNextButton());
});
test('should call the `onMonthChange` callback', () => {
expect(testContext.onMonthChange).toHaveBeenCalledWith(nextMonth);
});
});
describe('and the next month is not defined', () => {
const testContext = {
...dayPickerProps,
toDate: today,
onMonthChange: jest.fn()
};
beforeEach(async () => {
customRender(<CaptionNavigation displayMonth={today} />, testContext);
await user.click(getNextButton());
});
test('should call the `onMonthChange` callback', () => {
expect(testContext.onMonthChange).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,49 @@
import { MouseEventHandler } from 'react';
import { isSameMonth } from 'date-fns';
import { CaptionProps } from 'components/Caption/Caption';
import { Navigation } from 'components/Navigation';
import { useDayPicker } from 'contexts/DayPicker';
import { useNavigation } from 'contexts/Navigation';
/**
* Render a caption with a button-based navigation.
*/
export function CaptionNavigation(props: CaptionProps): JSX.Element {
const { numberOfMonths } = useDayPicker();
const { previousMonth, nextMonth, goToMonth, displayMonths } =
useNavigation();
const displayIndex = displayMonths.findIndex((month) =>
isSameMonth(props.displayMonth, month)
);
const isFirst = displayIndex === 0;
const isLast = displayIndex === displayMonths.length - 1;
const hideNext = numberOfMonths > 1 && (isFirst || !isLast);
const hidePrevious = numberOfMonths > 1 && (isLast || !isFirst);
const handlePreviousClick: MouseEventHandler = () => {
if (!previousMonth) return;
goToMonth(previousMonth);
};
const handleNextClick: MouseEventHandler = () => {
if (!nextMonth) return;
goToMonth(nextMonth);
};
return (
<Navigation
displayMonth={props.displayMonth}
hideNext={hideNext}
hidePrevious={hidePrevious}
nextMonth={nextMonth}
previousMonth={previousMonth}
onPreviousClick={handlePreviousClick}
onNextClick={handleNextClick}
/>
);
}

View File

@@ -0,0 +1 @@
export * from './CaptionNavigation';

View File

@@ -0,0 +1,82 @@
import { screen } from '@testing-library/react';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { freezeBeforeAll } from 'test/utils';
import { CustomComponents } from 'types/DayPickerBase';
import { Day, DayProps } from './Day';
const today = new Date(2021, 8);
freezeBeforeAll(today);
const date = today;
const displayMonth = today;
const props: DayProps = {
date: date,
displayMonth
};
describe('when the day to render has an hidden modifier', () => {
const dayPickerProps: DayPickerProps = {
modifiers: { hidden: date }
};
beforeEach(() => {
customRender(<Day {...props} />, dayPickerProps);
});
test('should render an empty grid cell', () => {
const cell = screen.getByRole('gridcell');
expect(cell).toBeEmptyDOMElement();
});
});
describe('when a no selection mode and no "onDayClick"', () => {
const dayPickerProps: DayPickerProps = { mode: 'default' };
beforeEach(() => {
customRender(<Day {...props} />, dayPickerProps);
});
test('should render a div', () => {
const cell = screen.getByRole('gridcell');
expect(cell.nodeName).toBe('DIV');
});
});
describe('when a selection mode is set', () => {
const dayPickerProps: DayPickerProps = {
mode: 'single'
};
beforeEach(() => {
customRender(<Day {...props} />, dayPickerProps);
});
test('should render a button named "day"', () => {
const cell = screen.getByRole('gridcell');
expect(cell.nodeName).toBe('BUTTON');
expect(cell).toHaveAttribute('name', 'day');
});
});
describe('when "onDayClick" is present', () => {
const dayPickerProps: DayPickerProps = {
onDayClick: jest.fn()
};
beforeEach(() => {
customRender(<Day {...props} />, dayPickerProps);
});
test('should render a button', () => {
const cell = screen.getByRole('gridcell');
expect(cell.nodeName).toBe('BUTTON');
});
});
describe('when using a custom DayContent component', () => {
const components: CustomComponents = {
DayContent: () => <>Custom DayContent</>
};
beforeEach(() => {
customRender(<Day {...props} />, { components });
});
test('it should render the custom component instead', () => {
expect(screen.getByText('Custom DayContent')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,30 @@
import { useRef } from 'react';
import { useDayRender } from 'hooks/useDayRender';
import { Button } from '../Button';
/** Represent the props used by the {@link Day} component. */
export interface DayProps {
/** The month where the date is displayed. */
displayMonth: Date;
/** The date to render. */
date: Date;
}
/**
* The content of a day cell as a button or span element according to its
* modifiers.
*/
export function Day(props: DayProps): JSX.Element {
const buttonRef = useRef<HTMLButtonElement>(null);
const dayRender = useDayRender(props.date, props.displayMonth, buttonRef);
if (dayRender.isHidden) {
return <div role="gridcell"></div>;
}
if (!dayRender.isButton) {
return <div {...dayRender.divProps} />;
}
return <Button name="day" ref={buttonRef} {...dayRender.buttonProps} />;
}

View File

@@ -0,0 +1 @@
export * from './Day';

View File

@@ -0,0 +1,37 @@
import { es } from 'date-fns/locale';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { freezeBeforeAll } from 'test/utils';
import { DayContent, DayContentProps } from 'components/DayContent';
const today = new Date(2021, 8);
freezeBeforeAll(today);
let container: HTMLElement;
function setup(props: DayContentProps, dayPickerProps?: DayPickerProps) {
const view = customRender(<DayContent {...props} />, dayPickerProps);
container = view.container;
}
const date = today;
const displayMonth = today;
const props: DayContentProps = {
date: date,
displayMonth,
activeModifiers: {}
};
const dayPickerProps: DayPickerProps = {
locale: es
};
describe('when rendered', () => {
beforeEach(() => {
setup(props, dayPickerProps);
});
test('contains the formatted day', () => {
expect(container.firstChild).toHaveTextContent('1');
});
});

View File

@@ -0,0 +1,22 @@
import { useDayPicker } from 'contexts/DayPicker';
import { ActiveModifiers } from 'types/Modifiers';
/** Represent the props for the {@link DayContent} component. */
export interface DayContentProps {
/** The date representing the day. */
date: Date;
/** The month where the day is displayed. */
displayMonth: Date;
/** The active modifiers for the given date. */
activeModifiers: ActiveModifiers;
}
/** Render the content of the day cell. */
export function DayContent(props: DayContentProps): JSX.Element {
const {
locale,
formatters: { formatDay }
} = useDayPicker();
return <>{formatDay(props.date, { locale })}</>;
}

View File

@@ -0,0 +1 @@
export * from './DayContent';

View File

@@ -0,0 +1,72 @@
import { fireEvent, screen } from '@testing-library/react';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { freezeBeforeAll } from 'test/utils';
import { Dropdown, DropdownProps } from 'components/Dropdown';
import { defaultClassNames } from 'contexts/DayPicker/defaultClassNames';
import { CustomComponents } from 'types/DayPickerBase';
const today = new Date(2021, 8);
freezeBeforeAll(today);
function setup(props: DropdownProps, dayPickerProps?: DayPickerProps) {
customRender(<Dropdown {...props} />, dayPickerProps);
}
const props: Required<DropdownProps> = {
name: 'dropdown',
'aria-label': 'foo',
onChange: jest.fn(),
caption: 'Some caption',
className: 'test',
value: 'bar',
children: <option value={'bar'} />,
style: {}
};
describe('when rendered', () => {
let combobox: HTMLElement;
let label: HTMLElement;
beforeEach(() => {
setup(props);
combobox = screen.getByRole('combobox');
label = screen.getByText(props['aria-label']);
});
test('should render the vhidden aria label', () => {
expect(label).toHaveClass(defaultClassNames.vhidden);
});
test('should render the combobox', () => {
expect(combobox).toBeInTheDocument();
});
describe('when the combobox changes', () => {
beforeEach(() => {
fireEvent.change(combobox);
});
test('should call the "onChange" eve, nt handler', () => {
expect(props.onChange).toHaveBeenCalled();
});
});
test('should render the combobox with the given value', () => {
expect(combobox).toHaveValue(props.value);
});
});
describe('when using a custom IconDropdown component', () => {
const components: CustomComponents = {
IconDropdown: () => <div>Custom IconDropdown</div>
};
beforeEach(() => {
setup(props, { components });
});
test('it should render the custom component instead', () => {
expect(screen.getByText('Custom IconDropdown')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,66 @@
import {
ChangeEventHandler,
CSSProperties,
ReactNode,
SelectHTMLAttributes
} from 'react';
import { IconDropdown } from 'components/IconDropdown';
import { useDayPicker } from 'contexts/DayPicker';
/** The props for the {@link Dropdown} component. */
export interface DropdownProps {
/** The name attribute of the element. */
name?: string;
/** The caption displayed to replace the hidden select. */
caption?: ReactNode;
children?: SelectHTMLAttributes<HTMLSelectElement>['children'];
className?: string;
['aria-label']?: string;
style?: CSSProperties;
/** The selected value. */
value?: string | number;
onChange?: ChangeEventHandler<HTMLSelectElement>;
}
/**
* Render a styled select component displaying a caption and a custom
* drop-down icon.
*/
export function Dropdown(props: DropdownProps): JSX.Element {
const { onChange, value, children, caption, className, style } = props;
const dayPicker = useDayPicker();
const IconDropdownComponent =
dayPicker.components?.IconDropdown ?? IconDropdown;
return (
<div className={className} style={style}>
<span className={dayPicker.classNames.vhidden}>
{props['aria-label']}
</span>
<select
name={props.name}
aria-label={props['aria-label']}
className={dayPicker.classNames.dropdown}
style={dayPicker.styles.dropdown}
value={value}
onChange={onChange}
>
{children}
</select>
<div
className={dayPicker.classNames.caption_label}
style={dayPicker.styles.caption_label}
aria-hidden="true"
>
{caption}
{
<IconDropdownComponent
className={dayPicker.classNames.dropdown_icon}
style={dayPicker.styles.dropdown_icon}
/>
}
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Dropdown';

View File

@@ -0,0 +1,27 @@
import { customRender } from 'test/render';
import { getTableFooter, queryTableFooter } from 'test/selectors';
import { Footer } from './Footer';
customRender(
<table role="grid">
<Footer />
</table>
);
test('should not render anything as default', () => {
expect(queryTableFooter()).toBeNull();
});
describe('when using the `footer` prop', () => {
beforeEach(() => {
customRender(
<table role="grid">
<Footer />
</table>,
{ footer: 'footer_foo' }
);
});
test('should render the table footer', () => {
expect(getTableFooter()).toHaveTextContent('footer_foo');
});
});

View File

@@ -0,0 +1,23 @@
import { useDayPicker } from 'contexts/DayPicker';
export interface FooterProps {
/** The month where the footer is displayed. */
displayMonth?: Date;
}
/** Render the Footer component (empty as default).*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function Footer(props: FooterProps): JSX.Element {
const {
footer,
styles,
classNames: { tfoot }
} = useDayPicker();
if (!footer) return <></>;
return (
<tfoot className={tfoot} style={styles.tfoot}>
<tr>
<td colSpan={8}>{footer}</td>
</tr>
</tfoot>
);
}

View File

@@ -0,0 +1 @@
export * from './Footer';

View File

@@ -0,0 +1,65 @@
import { RenderResult, screen } from '@testing-library/react';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { Head } from './Head';
let container: HTMLElement;
let view: RenderResult;
function setup(dayPickerProps: DayPickerProps = {}) {
view = customRender(
<table>
<Head />
</table>,
dayPickerProps
);
container = view.container.firstChild as HTMLTableCellElement;
}
const dayPickerProps = {
styles: {
head: { color: 'red' },
head_row: { color: 'blue' },
head_cell: { color: 'green' }
},
classNames: {
head: 'foo',
head_row: 'foo_row',
head_cell: 'foo_head-cell'
}
};
describe('when rendered', () => {
beforeEach(() => {
setup(dayPickerProps);
});
test('thead should have the `head` style', () => {
expect(container.firstChild).toHaveStyle(dayPickerProps.styles.head);
});
test('thead should have the `head` class', () => {
expect(container.firstChild).toHaveClass(dayPickerProps.classNames.head);
});
});
describe('when using a custom HeadRow component', () => {
beforeEach(() => {
setup({
...dayPickerProps,
components: {
HeadRow: () => (
<tr>
<td>custom head</td>
</tr>
)
}
});
});
test('should render the custom component', () => {
expect(screen.getByText('custom head')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,13 @@
import { HeadRow } from 'components/HeadRow';
import { useDayPicker } from 'contexts/DayPicker';
/** Render the table head. */
export function Head(): JSX.Element {
const { classNames, styles, components } = useDayPicker();
const HeadRowComponent = components?.HeadRow ?? HeadRow;
return (
<thead style={styles.head} className={classNames.head}>
<HeadRowComponent />
</thead>
);
}

View File

@@ -0,0 +1 @@
export * from './Head';

View File

@@ -0,0 +1,85 @@
import { RenderResult } from '@testing-library/react';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { HeadRow } from './HeadRow';
let container: HTMLElement;
let view: RenderResult;
let thElements: HTMLTableCellElement[];
function setup(dayPickerProps: DayPickerProps = {}) {
view = customRender(
<table>
<thead>
<HeadRow />
</thead>
</table>,
dayPickerProps
);
container = view.container.firstChild?.firstChild as HTMLTableRowElement;
thElements = Array.from(container.getElementsByTagName('th'));
}
const dayPickerProps = {
styles: {
head: { color: 'red' },
head_row: { color: 'blue' },
head_cell: { color: 'green' }
},
classNames: {
head: 'foo',
head_row: 'foo_row',
head_cell: 'foo_head-cell'
}
};
describe('when rendered', () => {
beforeEach(() => {
setup(dayPickerProps);
});
test('tr element should have the `head_row` style', () => {
expect(container.firstChild).toHaveStyle(dayPickerProps.styles.head_row);
});
test('tr element should have the `head_row` class', () => {
expect(container.firstChild).toHaveClass(
dayPickerProps.classNames.head_row
);
});
test('should render 7 head elements', () => {
expect(thElements).toHaveLength(7);
});
test('should render the head elements with the "head_cell" class name', () => {
thElements.forEach((el) => {
expect(el).toHaveClass(dayPickerProps.classNames.head_cell);
});
});
});
describe('when showing the week numbers', () => {
beforeEach(() => {
setup({ ...dayPickerProps, showWeekNumber: true });
});
test('should render 8 head elements', () => {
expect(thElements).toHaveLength(7);
});
test('should render the head elements with the "head_cell" class name', () => {
thElements.forEach((el) => {
expect(el).toHaveClass(dayPickerProps.classNames.head_cell);
});
});
test('should render the head elements with the "head_cell" style', () => {
thElements.forEach((el) => {
expect(el).toHaveStyle(dayPickerProps.styles.head_cell);
});
});
test('should render the head elements with the "col" scope', () => {
thElements.forEach((el) => {
expect(el).toHaveAttribute('scope', 'col');
});
});
});

View File

@@ -0,0 +1,40 @@
import { useDayPicker } from 'contexts/DayPicker';
import { getWeekdays } from './utils';
/**
* Render the HeadRow component - i.e. the table head row with the weekday names.
*/
export function HeadRow(): JSX.Element {
const {
classNames,
styles,
showWeekNumber,
locale,
weekStartsOn,
ISOWeek,
formatters: { formatWeekdayName },
labels: { labelWeekday }
} = useDayPicker();
const weekdays = getWeekdays(locale, weekStartsOn, ISOWeek);
return (
<tr style={styles.head_row} className={classNames.head_row}>
{showWeekNumber && (
<td style={styles.head_cell} className={classNames.head_cell}></td>
)}
{weekdays.map((weekday, i) => (
<th
key={i}
scope="col"
className={classNames.head_cell}
style={styles.head_cell}
aria-label={labelWeekday(weekday, { locale })}
>
{formatWeekdayName(weekday, { locale })}
</th>
))}
</tr>
);
}

View File

@@ -0,0 +1 @@
export * from './HeadRow';

View File

@@ -0,0 +1,46 @@
import { es } from 'date-fns/locale';
import { freezeBeforeAll } from 'test/utils';
import { getWeekdays } from './getWeekdays';
const today = new Date(2022, 1, 12);
const prevSunday = new Date(2022, 1, 6);
const prevMonday = new Date(2022, 1, 7);
freezeBeforeAll(today);
let result: Date[];
describe('when rendered without a locale', () => {
beforeEach(() => {
result = getWeekdays();
});
test('should return 7 days', () => {
expect(result).toHaveLength(7);
});
test('should return Sunday as first day', () => {
expect(result[0]).toEqual(prevSunday);
});
});
describe.each<0 | 1 | 2 | 3 | 4 | 5 | 6>([0, 1, 2, 3, 4, 5, 6])(
'when week start on %s',
(weekStartsOn) => {
beforeEach(() => {
result = getWeekdays(es, weekStartsOn);
});
test('the first date should be weekStartsOn', () => {
expect(result[0].getDay()).toBe(weekStartsOn);
});
}
);
describe('when using ISO week', () => {
beforeEach(() => {
result = getWeekdays(es, 3, true);
});
test('should return Monday as first day', () => {
expect(result[0]).toEqual(prevMonday);
});
});

View File

@@ -0,0 +1,24 @@
import { addDays, Locale, startOfISOWeek, startOfWeek } from 'date-fns';
/**
* Generate a series of 7 days, starting from the week, to use for formatting
* the weekday names (Monday, Tuesday, etc.).
*/
export function getWeekdays(
locale?: Locale,
/** The index of the first day of the week (0 - Sunday). */
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6,
/** Use ISOWeek instead of locale/ */
ISOWeek?: boolean
): Date[] {
const start = ISOWeek
? startOfISOWeek(new Date())
: startOfWeek(new Date(), { locale, weekStartsOn });
const days = [];
for (let i = 0; i < 7; i++) {
const day = addDays(start, i);
days.push(day);
}
return days;
}

View File

@@ -0,0 +1 @@
export * from './getWeekdays';

View File

@@ -0,0 +1,18 @@
import { customRender } from 'test/render';
import { IconDropdown } from './IconDropdown';
let root: HTMLElement;
beforeEach(() => {
const view = customRender(
<IconDropdown className="foo" style={{ color: 'red' }} />
);
root = view.container.firstChild as HTMLElement;
});
test('should add the class name', () => {
expect(root).toHaveClass('foo');
});
test('should apply the style', () => {
expect(root).toHaveStyle({ color: 'red' });
});

View File

@@ -0,0 +1,22 @@
import { StyledComponent } from 'types/Styles';
/**
* Render the icon in the styled drop-down.
*/
export function IconDropdown(props: StyledComponent): JSX.Element {
return (
<svg
width="8px"
height="8px"
viewBox="0 0 120 120"
data-testid="iconDropdown"
{...props}
>
<path
d="M4.22182541,48.2218254 C8.44222828,44.0014225 15.2388494,43.9273804 19.5496459,47.9996989 L19.7781746,48.2218254 L60,88.443 L100.221825,48.2218254 C104.442228,44.0014225 111.238849,43.9273804 115.549646,47.9996989 L115.778175,48.2218254 C119.998577,52.4422283 120.07262,59.2388494 116.000301,63.5496459 L115.778175,63.7781746 L67.7781746,111.778175 C63.5577717,115.998577 56.7611506,116.07262 52.4503541,112.000301 L52.2218254,111.778175 L4.22182541,63.7781746 C-0.0739418023,59.4824074 -0.0739418023,52.5175926 4.22182541,48.2218254 Z"
fill="currentColor"
fillRule="nonzero"
></path>
</svg>
);
}

View File

@@ -0,0 +1 @@
export * from './IconDropdown';

View File

@@ -0,0 +1,18 @@
import { customRender } from 'test/render';
import { IconLeft } from './IconLeft';
let root: HTMLElement;
beforeEach(() => {
const view = customRender(
<IconLeft className="foo" style={{ color: 'red' }} />
);
root = view.container.firstChild as HTMLElement;
});
test('should add the class name', () => {
expect(root).toHaveClass('foo');
});
test('should apply the style', () => {
expect(root).toHaveStyle({ color: 'red' });
});

View File

@@ -0,0 +1,16 @@
import { StyledComponent } from 'types/Styles';
/**
* Render the "previous month" button in the navigation.
*/
export function IconLeft(props: StyledComponent): JSX.Element {
return (
<svg width="16px" height="16px" viewBox="0 0 120 120" {...props}>
<path
d="M69.490332,3.34314575 C72.6145263,0.218951416 77.6798462,0.218951416 80.8040405,3.34314575 C83.8617626,6.40086786 83.9268205,11.3179931 80.9992143,14.4548388 L80.8040405,14.6568542 L35.461,60 L80.8040405,105.343146 C83.8617626,108.400868 83.9268205,113.317993 80.9992143,116.454839 L80.8040405,116.656854 C77.7463184,119.714576 72.8291931,119.779634 69.6923475,116.852028 L69.490332,116.656854 L18.490332,65.6568542 C15.4326099,62.5991321 15.367552,57.6820069 18.2951583,54.5451612 L18.490332,54.3431458 L69.490332,3.34314575 Z"
fill="currentColor"
fillRule="nonzero"
></path>
</svg>
);
}

View File

@@ -0,0 +1 @@
export * from './IconLeft';

View File

@@ -0,0 +1,18 @@
import { customRender } from 'test/render';
import { IconRight } from './IconRight';
let root: HTMLElement;
beforeEach(() => {
const view = customRender(
<IconRight className="foo" style={{ color: 'red' }} />
);
root = view.container.firstChild as HTMLElement;
});
test('should add the class name', () => {
expect(root).toHaveClass('foo');
});
test('should apply the style', () => {
expect(root).toHaveStyle({ color: 'red' });
});

View File

@@ -0,0 +1,15 @@
import { StyledComponent } from 'types/Styles';
/**
* Render the "next month" button in the navigation.
*/
export function IconRight(props: StyledComponent): JSX.Element {
return (
<svg width="16px" height="16px" viewBox="0 0 120 120" {...props}>
<path
d="M49.8040405,3.34314575 C46.6798462,0.218951416 41.6145263,0.218951416 38.490332,3.34314575 C35.4326099,6.40086786 35.367552,11.3179931 38.2951583,14.4548388 L38.490332,14.6568542 L83.8333725,60 L38.490332,105.343146 C35.4326099,108.400868 35.367552,113.317993 38.2951583,116.454839 L38.490332,116.656854 C41.5480541,119.714576 46.4651794,119.779634 49.602025,116.852028 L49.8040405,116.656854 L100.804041,65.6568542 C103.861763,62.5991321 103.926821,57.6820069 100.999214,54.5451612 L100.804041,54.3431458 L49.8040405,3.34314575 Z"
fill="currentColor"
></path>
</svg>
);
}

View File

@@ -0,0 +1 @@
export * from './IconRight';

View File

@@ -0,0 +1,230 @@
import { screen } from '@testing-library/react';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { getMonthCaption, getMonthGrid } from 'test/selectors';
import { CustomComponents } from 'types/DayPickerBase';
import { Month, MonthProps } from './Month';
let root: HTMLDivElement;
const displayMonth = new Date(2022, 10, 4);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testStyles: Record<string, any> = {
caption_start: { color: 'red' },
caption_end: { background: 'blue' },
caption_between: { fontSize: 20 }
};
const testClassNames: Record<string, string> = {
caption_start: 'caption_start',
caption_end: 'caption_end',
caption_between: 'caption_between'
};
type Test = {
monthProps: MonthProps;
dayPickerProps: DayPickerProps;
expected: string[];
notExpected: string[];
};
function setup(props: MonthProps, dayPickerProps?: DayPickerProps) {
const view = customRender(<Month {...props} />, dayPickerProps);
root = view.container.firstChild as HTMLDivElement;
}
describe('when rendered', () => {
beforeEach(() => {
setup({ displayIndex: 0, displayMonth });
});
test('the caption id should be the aria-labelledby of the grid', () => {
const captionId = getMonthCaption().getAttribute('id');
const gridLabelledBy = getMonthGrid().getAttribute('aria-labelledby');
expect(captionId).toEqual(gridLabelledBy);
});
});
describe('when rendered with a custom id', () => {
const id = 'custom-id';
beforeEach(() => {
setup({ displayIndex: 0, displayMonth }, { id });
});
test('the caption id should include the display index', () => {
const captionId = getMonthCaption().getAttribute('id');
expect(captionId).toEqual('custom-id-0');
});
test('the table id should include the display index', () => {
const tableId = getMonthGrid().getAttribute('id');
expect(tableId).toEqual('custom-id-grid-0');
});
});
describe('when using a custom Caption component', () => {
const components: CustomComponents = {
Caption: () => <>custom caption foo</>
};
beforeEach(() => {
setup({ displayIndex: 0, displayMonth }, { components });
});
test('it should render the custom component instead', () => {
expect(screen.getByText('custom caption foo')).toBeInTheDocument();
});
});
describe('when dir is ltr', () => {
const testLtr: Test[] = [
{
monthProps: {
displayIndex: 0,
displayMonth
},
dayPickerProps: {
numberOfMonths: 1,
styles: testStyles,
classNames: testClassNames
},
expected: ['caption_start', 'caption_end'],
notExpected: ['caption_between']
},
{
monthProps: {
displayIndex: 0,
displayMonth
},
dayPickerProps: {
numberOfMonths: 2,
styles: testStyles,
classNames: testClassNames
},
expected: ['caption_start'],
notExpected: ['caption_between', 'caption_end']
},
{
monthProps: {
displayIndex: 1,
displayMonth
},
dayPickerProps: {
numberOfMonths: 2,
styles: testStyles,
classNames: testClassNames
},
expected: ['caption_end'],
notExpected: ['caption_start', 'caption_between']
},
{
monthProps: {
displayIndex: 1,
displayMonth
},
dayPickerProps: {
numberOfMonths: 3,
styles: testStyles,
classNames: testClassNames
},
expected: ['caption_between'],
notExpected: ['caption_start', 'caption_end']
}
];
describe.each(testLtr)(
'when displayIndex is $monthProps.displayIndex and numberOfMonths is $dayPickerProps.numberOfMonths',
({ monthProps, dayPickerProps, expected, notExpected }) => {
beforeEach(() => {
setup(monthProps, dayPickerProps);
});
test.each(expected)(`the root should have the %s class`, (name) =>
expect(root).toHaveClass(testClassNames[name])
);
test.each(expected)(`the root should have the %s style`, (name) =>
expect(root).toHaveStyle(testStyles[name])
);
test.each(notExpected)(`the root should not have the %s class`, (name) =>
expect(root).not.toHaveClass(testClassNames[name])
);
}
);
});
describe('when dir is rtl', () => {
const testRtl: Test[] = [
{
monthProps: {
displayIndex: 0,
displayMonth
},
dayPickerProps: {
dir: 'rtl',
numberOfMonths: 1,
styles: testStyles,
classNames: testClassNames
},
expected: ['caption_start', 'caption_end'],
notExpected: ['caption_between']
},
{
monthProps: {
displayIndex: 0,
displayMonth
},
dayPickerProps: {
dir: 'rtl',
numberOfMonths: 2,
styles: testStyles,
classNames: testClassNames
},
expected: ['caption_end'],
notExpected: ['caption_between', 'caption_start']
},
{
monthProps: {
displayIndex: 1,
displayMonth
},
dayPickerProps: {
dir: 'rtl',
numberOfMonths: 2,
styles: testStyles,
classNames: testClassNames
},
expected: ['caption_start'],
notExpected: ['caption_end', 'caption_between']
},
{
monthProps: {
displayIndex: 1,
displayMonth
},
dayPickerProps: {
dir: 'rtl',
numberOfMonths: 3,
styles: testStyles,
classNames: testClassNames
},
expected: ['caption_between'],
notExpected: ['caption_start', 'caption_end']
}
];
describe.each(testRtl)(
'when displayIndex is $monthProps.displayIndex and numberOfMonths is $dayPickerProps.numberOfMonths',
({ monthProps, dayPickerProps, expected, notExpected }) => {
beforeEach(() => {
setup(monthProps, dayPickerProps);
});
test.each(expected)(`the root should have the %s class`, (name) =>
expect(root).toHaveClass(testClassNames[name])
);
test.each(expected)(`the root should have the %s style`, (name) =>
expect(root).toHaveStyle(testStyles[name])
);
test.each(notExpected)(`the root should not have the %s class`, (name) =>
expect(root).not.toHaveClass(testClassNames[name])
);
}
);
});

View File

@@ -0,0 +1,66 @@
import { Caption } from 'components/Caption';
import { Table } from 'components/Table';
import { useDayPicker } from 'contexts/DayPicker';
import { useNavigation } from 'contexts/Navigation';
import { useId } from 'hooks/useId';
/** The props for the {@link Month} component. */
export interface MonthProps {
displayIndex: number;
displayMonth: Date;
}
/** Render a month. */
export function Month(props: MonthProps) {
const dayPicker = useDayPicker();
const { dir, classNames, styles, components } = dayPicker;
const { displayMonths } = useNavigation();
const captionId = useId(
dayPicker.id ? `${dayPicker.id}-${props.displayIndex}` : undefined
);
const tableId = dayPicker.id
? `${dayPicker.id}-grid-${props.displayIndex}`
: undefined;
const className = [classNames.month];
let style = styles.month;
let isStart = props.displayIndex === 0;
let isEnd = props.displayIndex === displayMonths.length - 1;
const isCenter = !isStart && !isEnd;
if (dir === 'rtl') {
[isEnd, isStart] = [isStart, isEnd];
}
if (isStart) {
className.push(classNames.caption_start);
style = { ...style, ...styles.caption_start };
}
if (isEnd) {
className.push(classNames.caption_end);
style = { ...style, ...styles.caption_end };
}
if (isCenter) {
className.push(classNames.caption_between);
style = { ...style, ...styles.caption_between };
}
const CaptionComponent = components?.Caption ?? Caption;
return (
<div key={props.displayIndex} className={className.join(' ')} style={style}>
<CaptionComponent
id={captionId}
displayMonth={props.displayMonth}
displayIndex={props.displayIndex}
/>
<Table
id={tableId}
aria-labelledby={captionId}
displayMonth={props.displayMonth}
/>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Month';

View File

@@ -0,0 +1,27 @@
import { customRender } from 'test/render';
import { Months } from './Months';
let root: HTMLElement;
test('should use the default class name', () => {
const view = customRender(<Months>foo</Months>, {});
root = view.container.firstChild as HTMLElement;
expect(root).toHaveClass('rdp-months');
});
test('should use a custom class name', () => {
const view = customRender(<Months>foo</Months>, {
classNames: { months: 'foo' }
});
root = view.container.firstChild as HTMLElement;
expect(root).toHaveClass('foo');
});
test('should use a custom style', () => {
const view = customRender(<Months>foo</Months>, {
styles: { months: { color: 'red' } }
});
root = view.container.firstChild as HTMLElement;
expect(root).toHaveStyle({ color: 'red' });
});

View File

@@ -0,0 +1,19 @@
import { ReactNode } from 'react';
import { useDayPicker } from 'contexts/DayPicker';
/** The props for the {@link Months} component. */
export type MonthsProps = { children: ReactNode };
/**
* Render the wrapper for the month grids.
*/
export function Months(props: MonthsProps): JSX.Element {
const { classNames, styles } = useDayPicker();
return (
<div className={classNames.months} style={styles.months}>
{props.children}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Months';

View File

@@ -0,0 +1,107 @@
import { screen } from '@testing-library/react';
import { addMonths, differenceInMonths } from 'date-fns';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { user } from 'test/user';
import { freezeBeforeAll } from 'test/utils';
import { MonthsDropdown, MonthsDropdownProps } from './MonthsDropdown';
const today = new Date(2020, 12, 22);
freezeBeforeAll(today);
let root: HTMLDivElement;
let options: HTMLCollectionOf<HTMLOptionElement> | undefined;
let select: HTMLSelectElement | null;
function setup(props: MonthsDropdownProps, dayPickerProps?: DayPickerProps) {
const view = customRender(<MonthsDropdown {...props} />, dayPickerProps);
root = view.container.firstChild as HTMLDivElement;
select = screen.queryByRole('combobox', { name: 'Month:' });
options = select?.getElementsByTagName('option');
}
const props: MonthsDropdownProps = {
displayMonth: today,
onChange: jest.fn()
};
describe('when fromDate and toDate are passed in', () => {
beforeEach(() => {
setup(props, { fromDate: new Date(), toDate: addMonths(new Date(), 1) });
});
test('should render the dropdown element', () => {
expect(root).toMatchSnapshot();
expect(select).toHaveAttribute('name', 'months');
});
});
describe('when "fromDate" is not set', () => {
beforeEach(() => {
setup(props, { fromDate: undefined });
});
test('should return nothing', () => {
expect(root).toBeNull();
});
});
describe('when "toDate" is not set', () => {
beforeEach(() => {
setup(props, { toDate: undefined });
});
test('should return nothing', () => {
expect(root).toBeNull();
});
});
describe('when "fromDate" and "toDate" are in the same year', () => {
const fromDate = new Date(2012, 0, 22);
const toDate = new Date(2012, 10, 22);
beforeEach(() => {
setup(props, { fromDate, toDate });
});
test('should display the months included between the two dates', () => {
expect(options).toHaveLength(differenceInMonths(toDate, fromDate) + 1);
});
test('the first month should be the fromDate month', () => {
expect(options?.[0]).toHaveValue(String(fromDate.getMonth()));
});
test('the last month should be the toMonth month', () => {
expect(options?.[options.length - 1]).toHaveValue(
String(toDate.getMonth())
);
});
});
describe('when "fromDate" and "toDate" are not in the same year', () => {
const fromDate = new Date(2012, 0, 22);
const toDate = new Date(2015, 10, 22);
const displayMonth = new Date(2015, 7, 0);
beforeEach(() => {
setup({ ...props, displayMonth }, { fromDate, toDate });
});
test('should display the 12 months', () => {
expect(options).toHaveLength(12);
});
test('the first month should be January', () => {
expect(options?.[0]).toHaveValue('0');
});
test('the last month should be December', () => {
expect(options?.[options.length - 1]).toHaveValue('11');
});
test('should select the displayed month', () => {
expect(select).toHaveValue(`${displayMonth.getMonth()}`);
});
describe('when the dropdown changes', () => {
beforeEach(async () => {
if (select) await user.selectOptions(select, 'February');
});
test('should fire the "onChange" event handler', () => {
const expectedMonth = new Date(2015, 1, 1);
expect(props.onChange).toHaveBeenCalledWith(expectedMonth);
});
});
});

View File

@@ -0,0 +1,74 @@
import { ChangeEventHandler } from 'react';
import { isSameYear, setMonth, startOfMonth } from 'date-fns';
import { Dropdown } from 'components/Dropdown';
import { useDayPicker } from 'contexts/DayPicker';
import { MonthChangeEventHandler } from 'types/EventHandlers';
/** The props for the {@link MonthsDropdown} component. */
export interface MonthsDropdownProps {
/** The month where the dropdown is displayed. */
displayMonth: Date;
onChange: MonthChangeEventHandler;
}
/** Render the dropdown to navigate between months. */
export function MonthsDropdown(props: MonthsDropdownProps): JSX.Element {
const {
fromDate,
toDate,
styles,
locale,
formatters: { formatMonthCaption },
classNames,
components,
labels: { labelMonthDropdown }
} = useDayPicker();
// Dropdown should appear only when both from/toDate is set
if (!fromDate) return <></>;
if (!toDate) return <></>;
const dropdownMonths: Date[] = [];
if (isSameYear(fromDate, toDate)) {
// only display the months included in the range
const date = startOfMonth(fromDate);
for (let month = fromDate.getMonth(); month <= toDate.getMonth(); month++) {
dropdownMonths.push(setMonth(date, month));
}
} else {
// display all the 12 months
const date = startOfMonth(new Date()); // Any date should be OK, as we just need the year
for (let month = 0; month <= 11; month++) {
dropdownMonths.push(setMonth(date, month));
}
}
const handleChange: ChangeEventHandler<HTMLSelectElement> = (e) => {
const selectedMonth = Number(e.target.value);
const newMonth = setMonth(startOfMonth(props.displayMonth), selectedMonth);
props.onChange(newMonth);
};
const DropdownComponent = components?.Dropdown ?? Dropdown;
return (
<DropdownComponent
name="months"
aria-label={labelMonthDropdown()}
className={classNames.dropdown_month}
style={styles.dropdown_month}
onChange={handleChange}
value={props.displayMonth.getMonth()}
caption={formatMonthCaption(props.displayMonth, { locale })}
>
{dropdownMonths.map((m) => (
<option key={m.getMonth()} value={m.getMonth()}>
{formatMonthCaption(m, { locale })}
</option>
))}
</DropdownComponent>
);
}

View File

@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`when fromDate and toDate are passed in should render the dropdown element 1`] = `
<div
class="rdp-dropdown_month"
>
<span
class="rdp-vhidden"
>
Month:
</span>
<select
aria-label="Month: "
class="rdp-dropdown"
name="months"
>
<option
value="0"
>
January
</option>
<option
value="1"
>
February
</option>
</select>
<div
aria-hidden="true"
class="rdp-caption_label"
>
January
<svg
class="rdp-dropdown_icon"
data-testid="iconDropdown"
height="8px"
viewBox="0 0 120 120"
width="8px"
>
<path
d="M4.22182541,48.2218254 C8.44222828,44.0014225 15.2388494,43.9273804 19.5496459,47.9996989 L19.7781746,48.2218254 L60,88.443 L100.221825,48.2218254 C104.442228,44.0014225 111.238849,43.9273804 115.549646,47.9996989 L115.778175,48.2218254 C119.998577,52.4422283 120.07262,59.2388494 116.000301,63.5496459 L115.778175,63.7781746 L67.7781746,111.778175 C63.5577717,115.998577 56.7611506,116.07262 52.4503541,112.000301 L52.2218254,111.778175 L4.22182541,63.7781746 C-0.0739418023,59.4824074 -0.0739418023,52.5175926 4.22182541,48.2218254 Z"
fill="currentColor"
fill-rule="nonzero"
/>
</svg>
</div>
</div>
`;

View File

@@ -0,0 +1 @@
export * from './MonthsDropdown';

View File

@@ -0,0 +1,133 @@
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { getNextButton, getPrevButton } from 'test/selectors';
import { user } from 'test/user';
import { Navigation, NavigationProps } from './Navigation';
let root: HTMLElement;
function setup(props: NavigationProps, dayPickerProps?: DayPickerProps) {
const view = customRender(<Navigation {...props} />, dayPickerProps);
root = view.container.firstChild as HTMLElement;
}
const props: NavigationProps = {
previousMonth: new Date(2021, 3),
nextMonth: new Date(2021, 5),
displayMonth: new Date(2021, 4),
hidePrevious: false,
hideNext: false,
onNextClick: jest.fn(),
onPreviousClick: jest.fn()
};
const dayPickerProps = {
classNames: {
nav: 'foo'
},
styles: {
nav: { color: 'red' }
},
components: {
IconRight: () => <svg>IconRight</svg>,
IconLeft: () => <svg>IconLeft</svg>
}
};
describe('when rendered', () => {
beforeEach(() => {
setup(props, dayPickerProps);
});
test('should add the class name', () => {
expect(root).toHaveClass(dayPickerProps.classNames.nav);
});
test('should apply the style', () => {
expect(root).toHaveStyle(dayPickerProps.styles.nav);
});
test('the previous button should display the left icon', () => {
const icons = root.getElementsByTagName('svg');
expect(icons[0]).toHaveTextContent('IconLeft');
});
test('the next button should display the right icon', () => {
const icons = root.getElementsByTagName('svg');
expect(icons[1]).toHaveTextContent('IconRight');
});
test('the previous button should be named "previous-month"', () => {
expect(getPrevButton()).toHaveAttribute('name', 'previous-month');
});
test('the next button should be named "next-month"', () => {
expect(getNextButton()).toHaveAttribute('name', 'next-month');
});
beforeEach(async () => {
await user.click(getPrevButton());
});
test('should call "onPreviousClick"', () => {
expect(props.onPreviousClick).toHaveBeenCalled();
});
describe('when clicking the next button', () => {
beforeEach(async () => {
await user.click(getNextButton());
});
test('should call "onNextClick"', () => {
expect(props.onNextClick).toHaveBeenCalled();
});
});
});
describe('when in right-to-left direction', () => {
beforeEach(() => {
setup(props, { ...dayPickerProps, dir: 'rtl' });
});
test('the previous button should display the right icon', () => {
const icons = root.getElementsByTagName('svg');
expect(icons[0]).toHaveTextContent('IconRight');
});
test('the next button should display the left icon', () => {
const icons = root.getElementsByTagName('svg');
expect(icons[1]).toHaveTextContent('IconLeft');
});
describe('when clicking the previous button', () => {
beforeEach(async () => {
await user.click(getPrevButton());
});
test('should call "onPreviousClick"', () => {
expect(props.onPreviousClick).toHaveBeenCalled();
});
});
describe('when clicking the next button', () => {
beforeEach(async () => {
await user.click(getNextButton());
});
test('should call "onNextClick"', () => {
expect(props.onNextClick).toHaveBeenCalled();
});
});
});
describe('when the previous month is undefined', () => {
beforeEach(() => {
setup({ ...props, previousMonth: undefined }, dayPickerProps);
});
test('the previous button should be disabled', () => {
expect(getPrevButton()).toBeDisabled();
});
test('the next button should be enabled', () => {
expect(getNextButton()).toBeEnabled();
});
});
describe('when the next month is undefined', () => {
beforeEach(() => {
setup({ ...props, nextMonth: undefined }, dayPickerProps);
});
test('the previous button should be enabled', () => {
expect(getPrevButton()).toBeEnabled();
});
test('the next button should be disabled', () => {
expect(getNextButton()).toBeDisabled();
});
});

View File

@@ -0,0 +1,104 @@
import { MouseEventHandler } from 'react';
import { IconLeft } from 'components/IconLeft';
import { IconRight } from 'components/IconRight';
import { useDayPicker } from 'contexts/DayPicker';
import { Button } from '../Button';
/** The props for the {@link Navigation} component. */
export interface NavigationProps {
/** The month where the caption is displayed. */
displayMonth: Date;
/** The previous month. */
previousMonth?: Date;
/** The next month. */
nextMonth?: Date;
/** Hide the previous button. */
hidePrevious: boolean;
/** Hide the next button. */
hideNext: boolean;
/** Event handler when the next button is clicked. */
onNextClick: MouseEventHandler<HTMLButtonElement>;
/** Event handler when the previous button is clicked. */
onPreviousClick: MouseEventHandler<HTMLButtonElement>;
}
/** A component rendering the navigation buttons or the drop-downs. */
export function Navigation(props: NavigationProps): JSX.Element {
const {
dir,
locale,
classNames,
styles,
labels: { labelPrevious, labelNext },
components
} = useDayPicker();
if (!props.nextMonth && !props.previousMonth) {
return <></>;
}
const previousLabel = labelPrevious(props.previousMonth, { locale });
const previousClassName = [
classNames.nav_button,
classNames.nav_button_previous
].join(' ');
const nextLabel = labelNext(props.nextMonth, { locale });
const nextClassName = [
classNames.nav_button,
classNames.nav_button_next
].join(' ');
const IconRightComponent = components?.IconRight ?? IconRight;
const IconLeftComponent = components?.IconLeft ?? IconLeft;
return (
<div className={classNames.nav} style={styles.nav}>
{!props.hidePrevious && (
<Button
name="previous-month"
aria-label={previousLabel}
className={previousClassName}
style={styles.nav_button_previous}
disabled={!props.previousMonth}
onClick={props.onPreviousClick}
>
{dir === 'rtl' ? (
<IconRightComponent
className={classNames.nav_icon}
style={styles.nav_icon}
/>
) : (
<IconLeftComponent
className={classNames.nav_icon}
style={styles.nav_icon}
/>
)}
</Button>
)}
{!props.hideNext && (
<Button
name="next-month"
aria-label={nextLabel}
className={nextClassName}
style={styles.nav_button_next}
disabled={!props.nextMonth}
onClick={props.onNextClick}
>
{dir === 'rtl' ? (
<IconLeftComponent
className={classNames.nav_icon}
style={styles.nav_icon}
/>
) : (
<IconRightComponent
className={classNames.nav_icon}
style={styles.nav_icon}
/>
)}
</Button>
)}
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Navigation';

View File

@@ -0,0 +1,173 @@
import { RenderResult, screen } from '@testing-library/react';
import { addDays } from 'date-fns';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { getDayButton, queryMonthGrids } from 'test/selectors';
import { freezeBeforeAll } from 'test/utils';
import { MonthsProps } from 'components/Months';
import { defaultClassNames } from 'contexts/DayPicker/defaultClassNames';
import { ClassNames } from 'types/Styles';
import { Root } from './Root';
const today = new Date(2020, 10, 4);
freezeBeforeAll(today);
let container: HTMLElement;
let view: RenderResult;
function render(dayPickerProps: DayPickerProps = {}) {
view = customRender(<Root initialProps={dayPickerProps} />, dayPickerProps);
container = view.container;
}
describe('when the number of months is 1', () => {
const props: DayPickerProps = { numberOfMonths: 1 };
beforeEach(() => {
render(props);
});
test('should display one month grid', () => {
expect(queryMonthGrids()).toHaveLength(1);
});
});
describe('when the number of months is greater than 1', () => {
const props: DayPickerProps = { numberOfMonths: 3 };
beforeEach(() => {
render(props);
});
test('should display the specified number of month grids', () => {
expect(queryMonthGrids()).toHaveLength(3);
});
});
describe('when using the "classNames" prop', () => {
const classNames: ClassNames = {
root: 'foo'
};
beforeEach(() => {
render({ classNames });
});
test('should add the class to the container', () => {
expect(container.firstChild).toHaveClass('foo');
});
});
describe('when using a custom "Months" component', () => {
function CustomMonths(props: MonthsProps) {
return (
<div>
<div data-testid="foo" />
{props.children}
</div>
);
}
beforeEach(() => {
render({ numberOfMonths: 3, components: { Months: CustomMonths } });
});
test('should render the custom component', () => {
expect(screen.getByTestId('foo')).toBeInTheDocument();
});
test('should still display the specified number of months', () => {
expect(queryMonthGrids()).toHaveLength(3);
});
});
describe('when using the "id" prop', () => {
const testId = 'foo';
beforeEach(() => render({ id: testId }));
test('should add the "id" attribute', () => {
expect(container.firstChild).toHaveAttribute('id', testId);
});
});
describe('when using the "nonce" prop', () => {
const nonce = 'foo';
beforeEach(() => render({ nonce }));
test('should add the "nonce" attribute', () => {
expect(container.firstChild).toHaveAttribute('nonce', nonce);
});
});
describe('when using the "title" prop', () => {
const title = 'foo';
beforeEach(() => render({ title }));
test('should add the "title" attribute', () => {
expect(container.firstChild).toHaveAttribute('title', title);
});
});
describe('when using the "lang" prop', () => {
const lang = 'en-US';
beforeEach(() => render({ lang }));
test('should add the "lang" attribute', () => {
expect(container.firstChild).toHaveAttribute('lang', lang);
});
});
describe('when using the "className" prop', () => {
const props: DayPickerProps = { className: 'foo' };
beforeEach(() => {
render(props);
});
test('should append the class name to the root element', () => {
expect(container.firstChild).toHaveClass('rdp foo');
});
});
describe('when the "numberOfMonths" is greater than 1', () => {
const props: DayPickerProps = { numberOfMonths: 3 };
const expectedClassName = defaultClassNames.multiple_months;
beforeEach(() => {
render(props);
});
test(`should have the ${expectedClassName} class name`, () => {
expect(container.firstChild).toHaveClass(expectedClassName);
});
});
describe('when showing the week numbers', () => {
const props: DayPickerProps = { showWeekNumber: true };
const expectedClassName = defaultClassNames.with_weeknumber;
beforeEach(() => {
render(props);
});
test(`should have the ${expectedClassName} class name`, () => {
expect(container.firstChild).toHaveClass(expectedClassName);
});
});
describe('when "initialFocus" is set', () => {
const baseProps: DayPickerProps = {
initialFocus: true,
mode: 'single'
};
describe('when a day is not selected', () => {
beforeEach(() => {
render(baseProps);
});
test('should focus today', () => {
expect(getDayButton(today)).toHaveFocus();
});
describe('when a new day is focused', () => {
beforeEach(() => {
getDayButton(addDays(today, 1)).focus();
});
describe('and the calendar is rerendered', () => {
test.todo('should focus the new day');
});
});
});
describe('when a day is selected', () => {
const selected = addDays(today, 1);
const props: DayPickerProps = { ...baseProps, selected };
beforeEach(() => {
render(props);
});
test('should focus the selected day', () => {
expect(getDayButton(selected)).toHaveFocus();
});
});
});

View File

@@ -0,0 +1,89 @@
import { useEffect, useState } from 'react';
import { DayPickerProps } from 'DayPicker';
import { Month } from 'components/Month';
import { Months } from 'components/Months';
import { useDayPicker } from 'contexts/DayPicker';
import { useFocusContext } from 'contexts/Focus';
import { useNavigation } from 'contexts/Navigation';
function isDataAttributes(attrs: DayPickerProps): attrs is {
[key: string]: string | boolean | number | undefined;
} {
return true;
}
export interface RootProps {
initialProps: DayPickerProps;
}
/** Render the container with the months according to the number of months to display. */
export function Root({ initialProps }: RootProps): JSX.Element {
const dayPicker = useDayPicker();
const focusContext = useFocusContext();
const navigation = useNavigation();
const [hasInitialFocus, setHasInitialFocus] = useState(false);
// Focus the focus target when initialFocus is passed in
useEffect(() => {
if (!dayPicker.initialFocus) return;
if (!focusContext.focusTarget) return;
if (hasInitialFocus) return;
focusContext.focus(focusContext.focusTarget);
setHasInitialFocus(true);
}, [
dayPicker.initialFocus,
hasInitialFocus,
focusContext.focus,
focusContext.focusTarget,
focusContext
]);
// Apply classnames according to props
const classNames = [dayPicker.classNames.root, dayPicker.className];
if (dayPicker.numberOfMonths > 1) {
classNames.push(dayPicker.classNames.multiple_months);
}
if (dayPicker.showWeekNumber) {
classNames.push(dayPicker.classNames.with_weeknumber);
}
const style = {
...dayPicker.styles.root,
...dayPicker.style
};
const dataAttributes = Object.keys(initialProps)
.filter((key) => key.startsWith('data-'))
.reduce((attrs, key) => {
if (!isDataAttributes(initialProps)) return attrs;
return {
...attrs,
[key]: initialProps[key]
};
}, {});
const MonthsComponent = initialProps.components?.Months ?? Months;
return (
<div
className={classNames.join(' ')}
style={style}
dir={dayPicker.dir}
id={dayPicker.id}
nonce={initialProps.nonce}
title={initialProps.title}
lang={initialProps.lang}
{...dataAttributes}
>
<MonthsComponent>
{navigation.displayMonths.map((month, i) => (
<Month key={i} displayIndex={i} displayMonth={month} />
))}
</MonthsComponent>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from './Root';

View File

@@ -0,0 +1,67 @@
import { screen } from '@testing-library/react';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render/customRender';
import { CustomComponents } from 'types/DayPickerBase';
import { Row, RowProps } from './Row';
function setup(props: RowProps, dayPickerProps?: DayPickerProps) {
customRender(<Row {...props} />, dayPickerProps);
}
const props: RowProps = {
displayMonth: new Date(2020, 1),
weekNumber: 4,
dates: [new Date(2020, 1, 1), new Date(2020, 1, 2), new Date(2020, 1, 3)]
};
describe('when "showWeekNumber" is set', () => {
const dayPickerProps = {
showWeekNumber: true,
classNames: { cell: 'cell' },
styles: { cell: { background: 'red' } }
};
beforeEach(() => {
setup(props, dayPickerProps);
});
test('should display the cell with the week number', () => {
const cell = screen.getByRole('cell', { name: `${props.weekNumber}` });
expect(cell).toBeInTheDocument();
});
test('the cell should have the "cell" class name', () => {
const cell = screen.getByRole('cell', { name: `${props.weekNumber}` });
expect(cell).toHaveClass(dayPickerProps.classNames.cell);
});
test('the cell should have the "cell" style', () => {
const cell = screen.getByRole('cell', { name: `${props.weekNumber}` });
expect(cell).toHaveStyle(dayPickerProps.styles.cell);
});
});
describe('when using a custom Day component', () => {
const components: CustomComponents = {
Day: () => <div>CustomDay</div>
};
const dayPickerProps = { components };
beforeEach(() => {
setup(props, dayPickerProps);
});
test('it should render the custom component instead', () => {
expect(screen.getAllByText('CustomDay')).toHaveLength(props.dates.length);
});
});
describe('when using a custom WeekNumber component', () => {
const components: CustomComponents = {
WeekNumber: () => <div>WeekNumber</div>
};
const dayPickerProps: DayPickerProps = { components, showWeekNumber: true };
beforeEach(() => {
setup(props, dayPickerProps);
});
test('it should render the custom component instead', () => {
expect(screen.getByText('WeekNumber')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,50 @@
import { getUnixTime } from 'date-fns';
import { Day } from 'components/Day';
import { WeekNumber } from 'components/WeekNumber';
import { useDayPicker } from 'contexts/DayPicker';
/**
* The props for the {@link Row} component.
*/
export interface RowProps {
/** The month where the row is displayed. */
displayMonth: Date;
/** The number of the week to render. */
weekNumber: number;
/** The days contained in the week. */
dates: Date[];
}
/** Render a row in the calendar, with the days and the week number. */
export function Row(props: RowProps): JSX.Element {
const { styles, classNames, showWeekNumber, components } = useDayPicker();
const DayComponent = components?.Day ?? Day;
const WeeknumberComponent = components?.WeekNumber ?? WeekNumber;
let weekNumberCell;
if (showWeekNumber) {
weekNumberCell = (
<td className={classNames.cell} style={styles.cell}>
<WeeknumberComponent number={props.weekNumber} dates={props.dates} />
</td>
);
}
return (
<tr className={classNames.row} style={styles.row}>
{weekNumberCell}
{props.dates.map((date) => (
<td
className={classNames.cell}
style={styles.cell}
key={getUnixTime(date)}
role="presentation"
>
<DayComponent displayMonth={props.displayMonth} date={date} />
</td>
))}
</tr>
);
}

View File

@@ -0,0 +1 @@
export * from './Row';

View File

@@ -0,0 +1,62 @@
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render/customRender';
import { freezeBeforeAll } from 'test/utils';
import { FooterProps } from 'components/Footer';
import { Table, TableProps } from './Table';
function setup(props: TableProps, dayPickerProps?: DayPickerProps) {
return customRender(<Table {...props} />, dayPickerProps);
}
const today = new Date(2021, 11, 8);
freezeBeforeAll(today);
const props: TableProps = {
displayMonth: new Date(2020, 1)
};
test('should render correctly', () => {
const { container } = setup(props);
expect(container.firstChild).toMatchSnapshot();
});
describe('when showing the week numbers', () => {
const dayPickerProps = { showWeekNumber: true };
test('should render correctly', () => {
const { container } = setup(props, dayPickerProps);
expect(container.firstChild).toMatchSnapshot();
});
});
describe('when using custom components', () => {
const dayPickerProps: DayPickerProps = {
components: {
Head: () => (
<thead>
<tr>
<td>CustomHead</td>
</tr>
</thead>
),
Row: () => (
<tr>
<td>CustomRow</td>
</tr>
),
Footer: (props: FooterProps) => (
<tfoot>
<tr>
<td>{props.displayMonth?.toDateString()}</td>
</tr>
</tfoot>
)
}
};
test('should render correctly', () => {
const { container } = setup(props, dayPickerProps);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,65 @@
import { Footer } from 'components/Footer';
import { Head } from 'components/Head';
import { Row } from 'components/Row';
import { useDayPicker } from 'contexts/DayPicker';
import { getMonthWeeks } from './utils/getMonthWeeks';
/** The props for the {@link Table} component. */
export interface TableProps {
/** ID of table element */
id?: string;
/** The ID of the label of the table (the same given to the Caption). */
['aria-labelledby']?: string;
/** The month where the table is displayed. */
displayMonth: Date;
}
/** Render the table with the calendar. */
export function Table(props: TableProps): JSX.Element {
const {
locale,
classNames,
styles,
hideHead,
fixedWeeks,
components,
weekStartsOn,
firstWeekContainsDate,
ISOWeek
} = useDayPicker();
const weeks = getMonthWeeks(props.displayMonth, {
useFixedWeeks: Boolean(fixedWeeks),
ISOWeek,
locale,
weekStartsOn,
firstWeekContainsDate
});
const HeadComponent = components?.Head ?? Head;
const RowComponent = components?.Row ?? Row;
const FooterComponent = components?.Footer ?? Footer;
return (
<table
id={props.id}
className={classNames.table}
style={styles.table}
role="grid"
aria-labelledby={props['aria-labelledby']}
>
{!hideHead && <HeadComponent />}
<tbody className={classNames.tbody} style={styles.tbody}>
{weeks.map((week) => (
<RowComponent
displayMonth={props.displayMonth}
key={week.weekNumber}
dates={week.dates}
weekNumber={week.weekNumber}
/>
))}
</tbody>
<FooterComponent displayMonth={props.displayMonth} />
</table>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export * from './Table';

View File

@@ -0,0 +1,60 @@
import {
addDays,
differenceInCalendarDays,
endOfISOWeek,
endOfWeek,
getISOWeek,
getWeek,
Locale,
startOfISOWeek,
startOfWeek
} from 'date-fns';
import { MonthWeek } from './getMonthWeeks';
/** Return the weeks between two dates. */
export function daysToMonthWeeks(
fromDate: Date,
toDate: Date,
options?: {
ISOWeek?: boolean;
locale?: Locale;
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
firstWeekContainsDate?: 1 | 4;
}
): MonthWeek[] {
const toWeek = options?.ISOWeek
? endOfISOWeek(toDate)
: endOfWeek(toDate, options);
const fromWeek = options?.ISOWeek
? startOfISOWeek(fromDate)
: startOfWeek(fromDate, options);
const nOfDays = differenceInCalendarDays(toWeek, fromWeek);
const days: Date[] = [];
for (let i = 0; i <= nOfDays; i++) {
days.push(addDays(fromWeek, i));
}
const weeksInMonth = days.reduce((result: MonthWeek[], date) => {
const weekNumber = options?.ISOWeek
? getISOWeek(date)
: getWeek(date, options);
const existingWeek = result.find(
(value) => value.weekNumber === weekNumber
);
if (existingWeek) {
existingWeek.dates.push(date);
return result;
}
result.push({
weekNumber,
dates: [date]
});
return result;
}, []);
return weeksInMonth;
}

View File

@@ -0,0 +1,100 @@
import { enGB, enUS } from 'date-fns/locale';
import { getMonthWeeks } from './getMonthWeeks';
describe('when using the "enUS" locale', () => {
const locale = enUS;
describe('when using fixed weeks', () => {
const useFixedWeeks = true;
describe('when getting the weeks for December 2022', () => {
const date = new Date(2022, 11);
const weeks = getMonthWeeks(date, { useFixedWeeks, locale });
test('should return 49 - 1 week numbers', () => {
const weekNumbers = weeks.map((week) => week.weekNumber);
const expectedResult = [49, 50, 51, 52, 53, 1];
expect(weekNumbers).toEqual(expectedResult);
});
test('the last week should be the one in the next year', () => {
const lastWeek = weeks[weeks.length - 1];
const lastWeekDates = lastWeek.dates.map((date) => date.getDate());
const expectedResult = [1, 2, 3, 4, 5, 6, 7];
expect(lastWeekDates).toEqual(expectedResult);
});
});
describe('when getting the weeks for December 2021', () => {
const weeks = getMonthWeeks(new Date(2021, 11), {
useFixedWeeks: false,
locale: enUS
});
test('should return 49 - 1 week numbers', () => {
const weekNumbers = weeks.map((week) => week.weekNumber);
const expectedResult = [49, 50, 51, 52, 1];
expect(weekNumbers).toEqual(expectedResult);
});
test('the last week should be the last in the year', () => {
const lastWeek = weeks[weeks.length - 1];
const lastWeekDates = lastWeek.dates.map((date) => date.getDate());
const expectedResult = [26, 27, 28, 29, 30, 31, 1];
expect(lastWeekDates).toEqual(expectedResult);
});
test('week 1 contains the first day of the new year', () => {
expect(weeks[4].dates.map((date) => date.getDate())).toEqual([
26, 27, 28, 29, 30, 31, 1
]);
});
});
});
});
describe('when using the "enGB" locale', () => {
const locale = enGB;
describe('when getting the weeks for January 2022', () => {
const date = new Date(2022, 0);
const weeks = getMonthWeeks(date, { locale });
test('the first week should be the last of the previous year', () => {
const weekNumbers = weeks.map((week) => week.weekNumber);
expect(weekNumbers[0]).toEqual(52);
});
test('the first week should contain days from previous year', () => {
expect(weeks[0].dates.map((date) => date.getDate())).toEqual([
27, 28, 29, 30, 31, 1, 2
]);
});
test('the last week should be the last of January', () => {
const weekNumbers = weeks.map((week) => week.weekNumber);
expect(weekNumbers[weekNumbers.length - 1]).toEqual(5);
});
});
describe('when setting thursday as first day of year', () => {
const date = new Date(2022, 0);
const weeks = getMonthWeeks(date, { locale, firstWeekContainsDate: 4 });
test('the number of week should have number 52', () => {
const weekNumbers = weeks.map((week) => week.weekNumber);
expect(weekNumbers[0]).toEqual(52);
});
});
});
describe('when using the ISOWeek numbers', () => {
const locale = enUS;
describe('when getting the weeks for September 2022', () => {
const date = new Date(2022, 8);
const weeks = getMonthWeeks(date, { locale, ISOWeek: true });
test('the last week should have number 39', () => {
const weekNumbers = weeks.map((week) => week.weekNumber);
expect(weekNumbers[weekNumbers.length - 1]).toEqual(39);
});
});
});
describe('when not using the ISOWeek numbers', () => {
const locale = enUS;
describe('when getting the weeks for September 2022', () => {
const date = new Date(2022, 8);
const weeks = getMonthWeeks(date, { locale, ISOWeek: false });
test('the last week should have number 40', () => {
const weekNumbers = weeks.map((week) => week.weekNumber);
expect(weekNumbers[weekNumbers.length - 1]).toEqual(40);
});
});
});

View File

@@ -0,0 +1,55 @@
import {
addWeeks,
endOfMonth,
getWeeksInMonth,
Locale,
startOfMonth
} from 'date-fns';
import { daysToMonthWeeks } from './daysToMonthWeeks';
/** Represents a week in the month.*/
export type MonthWeek = {
/** The week number from the start of the year. */
weekNumber: number;
/** The dates in the week. */
dates: Date[];
};
/**
* Return the weeks belonging to the given month, adding the "outside days" to
* the first and last week.
*/
export function getMonthWeeks(
month: Date,
options: {
locale: Locale;
useFixedWeeks?: boolean;
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
firstWeekContainsDate?: 1 | 4;
ISOWeek?: boolean;
}
): MonthWeek[] {
const weeksInMonth: MonthWeek[] = daysToMonthWeeks(
startOfMonth(month),
endOfMonth(month),
options
);
if (options?.useFixedWeeks) {
// Add extra weeks to the month, up to 6 weeks
const nrOfMonthWeeks = getWeeksInMonth(month, options);
if (nrOfMonthWeeks < 6) {
const lastWeek = weeksInMonth[weeksInMonth.length - 1];
const lastDate = lastWeek.dates[lastWeek.dates.length - 1];
const toDate = addWeeks(lastDate, 6 - nrOfMonthWeeks);
const extraWeeks = daysToMonthWeeks(
addWeeks(lastDate, 1),
toDate,
options
);
weeksInMonth.push(...extraWeeks);
}
}
return weeksInMonth;
}

View File

@@ -0,0 +1,49 @@
import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render/customRender';
import { WeekNumber, WeekNumberProps } from './WeekNumber';
function setup(props: WeekNumberProps, dayPickerProps?: DayPickerProps) {
return customRender(<WeekNumber {...props} />, dayPickerProps);
}
const props: WeekNumberProps = {
number: 10,
dates: [new Date(), new Date()]
};
describe('without "onWeekNumberClick" prop', () => {
const dayPickerProps: DayPickerProps = { onWeekNumberClick: undefined };
test('it should return a span element', () => {
const { container } = setup(props, dayPickerProps);
expect(container.firstChild).toMatchSnapshot();
});
});
describe('with "onWeekNumberClick" prop', () => {
const dayPickerProps: DayPickerProps = { onWeekNumberClick: jest.fn() };
let container: HTMLElement;
beforeEach(() => {
container = setup(props, dayPickerProps).container;
});
test('it should return a button element', () => {
expect(screen.getByRole('button')).toBeInTheDocument();
expect(container.firstChild).toHaveAttribute('name', 'week-number');
expect(container.firstChild).toMatchSnapshot();
});
describe('when the button element is clicked', () => {
beforeEach(async () => {
await userEvent.click(screen.getByRole('button'));
});
test('should call onWeekNumberClick', () => {
expect(dayPickerProps.onWeekNumberClick).toHaveBeenCalledWith(
props.number,
props.dates,
expect.anything()
);
});
});
});

View File

@@ -0,0 +1,59 @@
import { MouseEventHandler } from 'react';
import { useDayPicker } from 'contexts/DayPicker';
import { Button } from '../Button';
/**
* The props for the {@link WeekNumber} component.
*/
export interface WeekNumberProps {
/** The number of the week. */
number: number;
/** The dates in the week. */
dates: Date[];
}
/**
* Render the week number element. If `onWeekNumberClick` is passed to DayPicker, it
* renders a button, otherwise a span element.
*/
export function WeekNumber(props: WeekNumberProps): JSX.Element {
const { number: weekNumber, dates } = props;
const {
onWeekNumberClick,
styles,
classNames,
locale,
labels: { labelWeekNumber },
formatters: { formatWeekNumber }
} = useDayPicker();
const content = formatWeekNumber(Number(weekNumber), { locale });
if (!onWeekNumberClick) {
return (
<span className={classNames.weeknumber} style={styles.weeknumber}>
{content}
</span>
);
}
const label = labelWeekNumber(Number(weekNumber), { locale });
const handleClick: MouseEventHandler = function (e) {
onWeekNumberClick(weekNumber, dates, e);
};
return (
<Button
name="week-number"
aria-label={label}
className={classNames.weeknumber}
style={styles.weeknumber}
onClick={handleClick}
>
{content}
</Button>
);
}

View File

@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`with "onWeekNumberClick" prop it should return a button element 1`] = `
<button
aria-label="Week n. 10"
class="rdp-button_reset rdp-button rdp-weeknumber"
name="week-number"
type="button"
>
10
</button>
`;
exports[`without "onWeekNumberClick" prop it should return a span element 1`] = `
<span
class="rdp-weeknumber"
>
10
</span>
`;

View File

@@ -0,0 +1 @@
export * from './WeekNumber';

View File

@@ -0,0 +1,106 @@
import { screen } from '@testing-library/react';
import { addMonths, differenceInYears } from 'date-fns';
import { DayPickerProps } from 'DayPicker';
import { customRender } from 'test/render';
import { user } from 'test/user';
import { freezeBeforeAll } from 'test/utils';
import { YearsDropdown, YearsDropdownProps } from './YearsDropdown';
const today = new Date(2020, 12, 22);
freezeBeforeAll(today);
let root: HTMLDivElement;
let options: HTMLCollectionOf<HTMLOptionElement> | undefined;
let select: HTMLSelectElement | null;
function setup(props: YearsDropdownProps, dayPickerProps?: DayPickerProps) {
const view = customRender(<YearsDropdown {...props} />, dayPickerProps);
root = view.container.firstChild as HTMLDivElement;
select = screen.queryByRole('combobox', { name: 'Year:' });
options = select?.getElementsByTagName('option');
}
const props: YearsDropdownProps = {
displayMonth: today,
onChange: jest.fn()
};
describe('when fromDate and toDate are passed in', () => {
beforeEach(() => {
setup(props, { fromDate: new Date(), toDate: addMonths(new Date(), 1) });
});
test('should render the dropdown element', () => {
expect(root).toMatchSnapshot();
expect(select).toHaveAttribute('name', 'years');
});
});
describe('when "fromDate" is not set', () => {
beforeEach(() => {
setup(props, { fromDate: undefined });
});
test('should return nothing', () => {
expect(root).toBeNull();
});
});
describe('when "toDate" is not set', () => {
beforeEach(() => {
setup(props, { toDate: undefined });
});
test('should return nothing', () => {
expect(root).toBeNull();
});
});
describe('when "fromDate" and "toDate" are in the same year', () => {
const fromDate = new Date(2012, 0, 22);
const toDate = new Date(2012, 10, 22);
beforeEach(() => {
setup(props, { fromDate, toDate });
});
test('should display the months included between the two dates', () => {
expect(select).toBeInTheDocument();
expect(options).toHaveLength(differenceInYears(toDate, fromDate) + 1);
});
test('the month should be the same month', () => {
expect(options?.[0]).toHaveValue(`${fromDate.getFullYear()}`);
});
});
describe('when "fromDate" and "toDate" are not in the same year', () => {
const fromDate = new Date(2012, 0, 22);
const toDate = new Date(2015, 10, 22);
const displayMonth = new Date(2013, 7, 0);
beforeEach(() => {
setup({ ...props, displayMonth }, { fromDate, toDate });
});
test('should display the full years', () => {
expect(options).toHaveLength(differenceInYears(toDate, fromDate) + 1);
});
test('the first option should be fromDates year', () => {
expect(options?.[0]).toHaveValue(`${fromDate.getFullYear()}`);
});
test('the last option should be "toDate"s year', () => {
expect(options?.[options.length - 1]).toHaveValue(
`${toDate.getFullYear()}`
);
});
test('should select the displayed year', () => {
expect(select).toHaveValue(`${displayMonth.getFullYear()}`);
});
describe('when the dropdown changes', () => {
const newYear = fromDate.getFullYear();
beforeEach(async () => {
if (select) await user.selectOptions(select, `${newYear}`);
});
test('should fire the "onChange" event handler', () => {
const expectedYear = new Date(newYear, displayMonth.getMonth(), 1);
expect(props.onChange).toHaveBeenCalledWith(expectedYear);
});
});
});

View File

@@ -0,0 +1,75 @@
import { ChangeEventHandler } from 'react';
import { setYear, startOfMonth, startOfYear } from 'date-fns';
import { Dropdown } from 'components/Dropdown';
import { useDayPicker } from 'contexts/DayPicker';
import { MonthChangeEventHandler } from 'types/EventHandlers';
/**
* The props for the {@link YearsDropdown} component.
*/
export interface YearsDropdownProps {
/** The month where the drop-down is displayed. */
displayMonth: Date;
/** Callback to handle the `change` event. */
onChange: MonthChangeEventHandler;
}
/**
* Render a dropdown to change the year. Take in account the `nav.fromDate` and
* `toDate` from context.
*/
export function YearsDropdown(props: YearsDropdownProps): JSX.Element {
const { displayMonth } = props;
const {
fromDate,
toDate,
locale,
styles,
classNames,
components,
formatters: { formatYearCaption },
labels: { labelYearDropdown }
} = useDayPicker();
const years: Date[] = [];
// Dropdown should appear only when both from/toDate is set
if (!fromDate) return <></>;
if (!toDate) return <></>;
const fromYear = fromDate.getFullYear();
const toYear = toDate.getFullYear();
for (let year = fromYear; year <= toYear; year++) {
years.push(setYear(startOfYear(new Date()), year));
}
const handleChange: ChangeEventHandler<HTMLSelectElement> = (e) => {
const newMonth = setYear(
startOfMonth(displayMonth),
Number(e.target.value)
);
props.onChange(newMonth);
};
const DropdownComponent = components?.Dropdown ?? Dropdown;
return (
<DropdownComponent
name="years"
aria-label={labelYearDropdown()}
className={classNames.dropdown_year}
style={styles.dropdown_year}
onChange={handleChange}
value={displayMonth.getFullYear()}
caption={formatYearCaption(displayMonth, { locale })}
>
{years.map((year) => (
<option key={year.getFullYear()} value={year.getFullYear()}>
{formatYearCaption(year, { locale })}
</option>
))}
</DropdownComponent>
);
}

View File

@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`when fromDate and toDate are passed in should render the dropdown element 1`] = `
<div
class="rdp-dropdown_year"
>
<span
class="rdp-vhidden"
>
Year:
</span>
<select
aria-label="Year: "
class="rdp-dropdown"
name="years"
>
<option
value="2021"
>
2021
</option>
</select>
<div
aria-hidden="true"
class="rdp-caption_label"
>
2021
<svg
class="rdp-dropdown_icon"
data-testid="iconDropdown"
height="8px"
viewBox="0 0 120 120"
width="8px"
>
<path
d="M4.22182541,48.2218254 C8.44222828,44.0014225 15.2388494,43.9273804 19.5496459,47.9996989 L19.7781746,48.2218254 L60,88.443 L100.221825,48.2218254 C104.442228,44.0014225 111.238849,43.9273804 115.549646,47.9996989 L115.778175,48.2218254 C119.998577,52.4422283 120.07262,59.2388494 116.000301,63.5496459 L115.778175,63.7781746 L67.7781746,111.778175 C63.5577717,115.998577 56.7611506,116.07262 52.4503541,112.000301 L52.2218254,111.778175 L4.22182541,63.7781746 C-0.0739418023,59.4824074 -0.0739418023,52.5175926 4.22182541,48.2218254 Z"
fill="currentColor"
fill-rule="nonzero"
/>
</svg>
</div>
</div>
`;

View File

@@ -0,0 +1 @@
export * from './YearsDropdown';

View File

@@ -0,0 +1,210 @@
/* eslint-disable testing-library/render-result-naming-convention */
import { es } from 'date-fns/locale';
import { DayPickerProps } from 'DayPicker';
import { renderDayPickerHook } from 'test/render';
import { freezeBeforeAll } from 'test/utils';
import { CaptionLayout } from 'components/Caption';
import { DayPickerContextValue, useDayPicker } from 'contexts/DayPicker';
import {
DefaultContextProps,
getDefaultContextValues
} from 'contexts/DayPicker/defaultContextValues';
import { DaySelectionMode } from 'types/DayPickerBase';
import { Formatters } from 'types/Formatters';
import { Labels } from 'types/Labels';
import { DayModifiers, ModifiersClassNames } from 'types/Modifiers';
import { ClassNames, Styles } from 'types/Styles';
const today = new Date(2022, 5, 13);
const defaults = getDefaultContextValues();
freezeBeforeAll(today);
function renderHook(props?: DayPickerProps) {
return renderDayPickerHook<DayPickerContextValue>(useDayPicker, props);
}
describe('when rendered without props', () => {
const testPropNames = Object.keys(defaults).filter(
(key) => key !== 'today'
) as DefaultContextProps[];
test.each(testPropNames)('should use the %s default value', (propName) => {
const result = renderHook();
expect(result.current[propName]).toEqual(defaults[propName]);
});
});
describe('when passing "locale" from props', () => {
const locale = es;
test('should return the custom locale', () => {
const result = renderHook({ locale });
expect(result.current.locale).toBe(locale);
});
});
describe('when passing "numberOfMonths" from props', () => {
const numberOfMonths = 4;
test('should return the custom numberOfMonths', () => {
const result = renderHook({ numberOfMonths });
expect(result.current.numberOfMonths).toBe(4);
});
});
describe('when passing "today" from props', () => {
const today = new Date(2010, 9, 11);
test('should return the custom "today"', () => {
const result = renderHook({ today });
expect(result.current.today).toBe(today);
});
});
describe('when passing "captionLayout" from props', () => {
const captionLayout: CaptionLayout = 'dropdown';
const fromYear = 2000;
const toYear = 2010;
const dayPickerProps: DayPickerProps = { captionLayout, fromYear, toYear };
test('should return the custom "captionLayout"', () => {
const result = renderHook(dayPickerProps);
expect(result.current.captionLayout).toBe(captionLayout);
});
});
describe('when "fromDate" and "toDate" are undefined', () => {
const fromDate = undefined;
const toDate = undefined;
describe('when using "dropdown" as "captionLayout"', () => {
const captionLayout: CaptionLayout = 'dropdown';
test('should return "buttons" as "captionLayout"', () => {
const result = renderHook({
fromDate,
toDate,
captionLayout
});
expect(result.current.captionLayout).toBe('buttons');
});
});
});
describe('when "fromDate" is undefined, but not "toDate"', () => {
const fromDate = undefined;
const toDate = new Date();
describe('when using "dropdown" as "captionLayout"', () => {
const captionLayout: CaptionLayout = 'dropdown';
test('should return "buttons" as "captionLayout"', () => {
const result = renderHook({
fromDate,
toDate,
captionLayout
});
expect(result.current.captionLayout).toBe('buttons');
});
});
});
describe('when "toDate" is undefined, but not "fromDate"', () => {
const fromDate = new Date();
const toDate = undefined;
describe('when using "dropdown" as "captionLayout"', () => {
const captionLayout: CaptionLayout = 'dropdown';
test('should return "buttons" as "captionLayout"', () => {
const result = renderHook({
fromDate,
toDate,
captionLayout
});
expect(result.current.captionLayout).toBe('buttons');
});
});
});
describe('when using "dropdown" as "captionLayout"', () => {
const captionLayout: CaptionLayout = 'dropdown';
const fromYear = 2000;
const toYear = 2010;
test('should return the custom "captionLayout"', () => {
const result = renderHook({ captionLayout, fromYear, toYear });
expect(result.current.captionLayout).toBe(captionLayout);
});
});
describe('when passing "modifiers" from props', () => {
const modifiers: DayModifiers = { foo: new Date() };
test('should return the custom "modifiers"', () => {
const result = renderHook({ modifiers });
expect(result.current.modifiers).toStrictEqual(modifiers);
});
});
describe('when passing "modifiersClassNames" from props', () => {
const modifiersClassNames: ModifiersClassNames = { foo: 'bar' };
test('should return the custom "modifiersClassNames"', () => {
const result = renderHook({ modifiersClassNames });
expect(result.current.modifiersClassNames).toStrictEqual(
modifiersClassNames
);
});
});
describe('when passing "styles" from props', () => {
const styles: Styles = { caption: { color: 'red ' } };
test('should include the custom "styles"', () => {
const result = renderHook({ styles });
expect(result.current.styles).toStrictEqual({
...defaults.styles,
...styles
});
});
});
describe('when passing "classNames" from props', () => {
const classNames: ClassNames = { caption: 'foo' };
test('should include the custom "classNames"', () => {
const result = renderHook({ classNames });
expect(result.current.classNames).toStrictEqual({
...defaults.classNames,
...classNames
});
});
});
describe('when passing "formatters" from props', () => {
const formatters: Partial<Formatters> = { formatCaption: jest.fn() };
test('should include the custom "formatters"', () => {
const result = renderHook({ formatters });
expect(result.current.formatters).toStrictEqual({
...defaults.formatters,
...formatters
});
});
});
describe('when passing "labels" from props', () => {
const labels: Partial<Labels> = { labelDay: jest.fn() };
test('should include the custom "labels"', () => {
const result = renderHook({ labels });
expect(result.current.labels).toStrictEqual({
...defaults.labels,
...labels
});
});
});
describe('when passing an "id" from props', () => {
test('should return the id', () => {
const result = renderHook({ id: 'foo' });
expect(result.current.id).toBe('foo');
});
});
describe('when in selection mode', () => {
const mode: DaySelectionMode = 'multiple';
const onSelect = jest.fn();
test('should return the "onSelect" event handler', () => {
const result = renderHook({ mode, onSelect });
expect(result.current.onSelect).toBe(onSelect);
});
});

View File

@@ -0,0 +1,150 @@
import { createContext, ReactNode, useContext } from 'react';
import { Locale } from 'date-fns';
import { DayPickerProps } from 'DayPicker';
import { CaptionLayout } from 'components/Caption';
import { DayPickerBase, DaySelectionMode } from 'types/DayPickerBase';
import {
DayPickerMultipleProps,
isDayPickerMultiple
} from 'types/DayPickerMultiple';
import { DayPickerRangeProps, isDayPickerRange } from 'types/DayPickerRange';
import { DayPickerSingleProps, isDayPickerSingle } from 'types/DayPickerSingle';
import { Formatters } from 'types/Formatters';
import { Labels } from 'types/Labels';
import { Matcher } from 'types/Matchers';
import { DayModifiers, ModifiersClassNames } from 'types/Modifiers';
import { ClassNames, Styles } from 'types/Styles';
import { getDefaultContextValues } from './defaultContextValues';
import { parseFromToProps } from './utils';
/**
* The value of the {@link DayPickerContext} extends the props from DayPicker
* with default and cleaned up values.
*/
export interface DayPickerContextValue extends DayPickerBase {
mode: DaySelectionMode;
onSelect?:
| DayPickerSingleProps['onSelect']
| DayPickerMultipleProps['onSelect']
| DayPickerRangeProps['onSelect'];
required?: boolean;
min?: number;
max?: number;
selected?: Matcher | Matcher[];
captionLayout: CaptionLayout;
classNames: Required<ClassNames>;
formatters: Formatters;
labels: Labels;
locale: Locale;
modifiersClassNames: ModifiersClassNames;
modifiers: DayModifiers;
numberOfMonths: number;
styles: Styles;
today: Date;
}
/**
* The DayPicker context shares the props passed to DayPicker within internal
* and custom components. It is used to set the default values and perform
* one-time calculations required to render the days.
*
* Access to this context from the {@link useDayPicker} hook.
*/
export const DayPickerContext = createContext<
DayPickerContextValue | undefined
>(undefined);
/** The props for the {@link DayPickerProvider}. */
export interface DayPickerProviderProps {
/** The initial props from the DayPicker component. */
initialProps: DayPickerProps;
children?: ReactNode;
}
/**
* The provider for the {@link DayPickerContext}, assigning the defaults from the
* initial DayPicker props.
*/
export function DayPickerProvider(props: DayPickerProviderProps): JSX.Element {
const { initialProps } = props;
const defaultContextValues = getDefaultContextValues();
const { fromDate, toDate } = parseFromToProps(initialProps);
let captionLayout =
initialProps.captionLayout ?? defaultContextValues.captionLayout;
if (captionLayout !== 'buttons' && (!fromDate || !toDate)) {
// When no from/to dates are set, the caption is always buttons
captionLayout = 'buttons';
}
let onSelect;
if (
isDayPickerSingle(initialProps) ||
isDayPickerMultiple(initialProps) ||
isDayPickerRange(initialProps)
) {
onSelect = initialProps.onSelect;
}
const value: DayPickerContextValue = {
...defaultContextValues,
...initialProps,
captionLayout,
classNames: {
...defaultContextValues.classNames,
...initialProps.classNames
},
components: {
...initialProps.components
},
formatters: {
...defaultContextValues.formatters,
...initialProps.formatters
},
fromDate,
labels: {
...defaultContextValues.labels,
...initialProps.labels
},
mode: initialProps.mode || defaultContextValues.mode,
modifiers: {
...defaultContextValues.modifiers,
...initialProps.modifiers
},
modifiersClassNames: {
...defaultContextValues.modifiersClassNames,
...initialProps.modifiersClassNames
},
onSelect,
styles: {
...defaultContextValues.styles,
...initialProps.styles
},
toDate
};
return (
<DayPickerContext.Provider value={value}>
{props.children}
</DayPickerContext.Provider>
);
}
/**
* Hook to access the {@link DayPickerContextValue}.
*
* Use the DayPicker context to access to the props passed to DayPicker inside
* internal or custom components.
*/
export function useDayPicker(): DayPickerContextValue {
const context = useContext(DayPickerContext);
if (!context) {
throw new Error(`useDayPicker must be used within a DayPickerProvider.`);
}
return context;
}

View File

@@ -0,0 +1,58 @@
import { ClassNames } from 'types/Styles';
/**
* The name of the default CSS classes.
*/
export const defaultClassNames: Required<ClassNames> = {
root: 'rdp',
multiple_months: 'rdp-multiple_months',
with_weeknumber: 'rdp-with_weeknumber',
vhidden: 'rdp-vhidden',
button_reset: 'rdp-button_reset',
button: 'rdp-button',
caption: 'rdp-caption',
caption_start: 'rdp-caption_start',
caption_end: 'rdp-caption_end',
caption_between: 'rdp-caption_between',
caption_label: 'rdp-caption_label',
caption_dropdowns: 'rdp-caption_dropdowns',
dropdown: 'rdp-dropdown',
dropdown_month: 'rdp-dropdown_month',
dropdown_year: 'rdp-dropdown_year',
dropdown_icon: 'rdp-dropdown_icon',
months: 'rdp-months',
month: 'rdp-month',
table: 'rdp-table',
tbody: 'rdp-tbody',
tfoot: 'rdp-tfoot',
head: 'rdp-head',
head_row: 'rdp-head_row',
head_cell: 'rdp-head_cell',
nav: 'rdp-nav',
nav_button: 'rdp-nav_button',
nav_button_previous: 'rdp-nav_button_previous',
nav_button_next: 'rdp-nav_button_next',
nav_icon: 'rdp-nav_icon',
row: 'rdp-row',
weeknumber: 'rdp-weeknumber',
cell: 'rdp-cell',
day: 'rdp-day',
day_today: 'rdp-day_today',
day_outside: 'rdp-day_outside',
day_selected: 'rdp-day_selected',
day_disabled: 'rdp-day_disabled',
day_hidden: 'rdp-day_hidden',
day_range_start: 'rdp-day_range_start',
day_range_end: 'rdp-day_range_end',
day_range_middle: 'rdp-day_range_middle'
};

View File

@@ -0,0 +1,54 @@
import { enUS } from 'date-fns/locale';
import { CaptionLayout } from 'components/Caption';
import { DayPickerContextValue } from 'contexts/DayPicker';
import { defaultClassNames } from './defaultClassNames';
import * as formatters from './formatters';
import * as labels from './labels';
export type DefaultContextProps =
| 'captionLayout'
| 'classNames'
| 'formatters'
| 'locale'
| 'labels'
| 'modifiersClassNames'
| 'modifiers'
| 'numberOfMonths'
| 'styles'
| 'today'
| 'mode';
export type DefaultContextValues = Pick<
DayPickerContextValue,
DefaultContextProps
>;
/**
* Returns the default values to use in the DayPickerContext, in case they are
* not passed down with the DayPicker initial props.
*/
export function getDefaultContextValues(): DefaultContextValues {
const captionLayout: CaptionLayout = 'buttons';
const classNames = defaultClassNames;
const locale = enUS;
const modifiersClassNames = {};
const modifiers = {};
const numberOfMonths = 1;
const styles = {};
const today = new Date();
return {
captionLayout,
classNames,
formatters,
labels,
locale,
modifiersClassNames,
modifiers,
numberOfMonths,
styles,
today,
mode: 'default'
};
}

Some files were not shown because too many files have changed in this diff Show More