├── .eslintrc
├── .github
├── logo.png
└── preview.gif
├── .gitignore
├── .storybook
├── addons.js
├── config.js
└── webpack.config.js
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── demo
└── src
│ ├── demo.css
│ └── index.js
├── examples
├── basic-setup.js
├── keyboard-support.js
├── localization.js
├── range-selection.js
└── with-multiple-dates.js
├── nwb.config.js
├── package.json
├── preprocess-css.js
├── src
├── .stories
│ ├── index.js
│ └── stories.scss
├── Calendar
│ ├── Container.scss
│ ├── index.js
│ ├── withDateSelection.js
│ ├── withKeyboardSupport.js
│ ├── withMultipleDates.js
│ └── withRange.js
├── Day
│ ├── Day.scss
│ └── index.js
├── Header
│ ├── Animation.scss
│ ├── Header.scss
│ ├── Slider
│ │ ├── Slider.scss
│ │ ├── index.js
│ │ └── transition.scss
│ ├── defaultSelectionRenderer.js
│ ├── index.js
│ ├── withMultipleDates.js
│ └── withRange.js
├── Month
│ ├── Month.scss
│ └── index.js
├── MonthList
│ ├── MonthList.scss
│ └── index.js
├── Today
│ ├── Today.scss
│ └── index.js
├── Weekdays
│ ├── Weekdays.scss
│ └── index.js
├── Years
│ ├── Years.scss
│ └── index.js
├── _variables.scss
├── index.js
└── utils
│ ├── animate.js
│ ├── defaultDisplayOptions.js
│ ├── defaultLocale.js
│ ├── defaultTheme.js
│ └── index.js
├── styles.css
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app",
3 | "rules": {
4 | "comma-dangle": ["error", "always-multiline"],
5 | "indent": ["error", 2, {"SwitchCase": 1}],
6 | "semi": ["error", "always"],
7 | "sort-keys": ["warn", "asc", {"caseSensitive": false, "natural": true}],
8 | "no-mixed-operators": [
9 | "warn",
10 | {
11 | "groups": [
12 | ['&', '|', '^', '~', '<<', '>>', '>>>'],
13 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='],
14 | ['in', 'instanceof']
15 | ],
16 | "allowSamePrecedence": true
17 | }
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clauderic/react-infinite-calendar/d7a77fc1508cb4ded30e47baabb111a8d6461721/.github/logo.png
--------------------------------------------------------------------------------
/.github/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clauderic/react-infinite-calendar/d7a77fc1508cb4ded30e47baabb111a8d6461721/.github/preview.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /demo/dist
3 | /es
4 | /lib
5 | /node_modules
6 | /umd
7 | npm-debug.log*
8 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@kadira/storybook-addon-options/register';
2 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import {configure} from '@kadira/storybook';
2 | import {setOptions} from '@kadira/storybook-addon-options';
3 |
4 | setOptions({
5 | name: 'INFINITE CALENDAR',
6 | url: 'https://github.com/clauderic/react-infinite-calendar',
7 | goFullScreen: false,
8 | showLeftPanel: true,
9 | showDownPanel: false,
10 | showSearchBox: false,
11 | downPanelInRight: false,
12 | sortStoriesByKind: false,
13 | });
14 |
15 | configure(() => require('../src/.stories/index.js'), module);
16 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | module: {
3 | loaders: [
4 | {
5 | test: /(\.scss)$/,
6 | loaders: [
7 | 'style',
8 | 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]',
9 | 'sass?sourceMap'
10 | ]
11 | }
12 | ]
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to this project
2 |
3 | First of all, thanks for contributing! Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved.
4 |
5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features.
6 |
7 | ## Using the issue tracker
8 |
9 | The issue tracker is the preferred channel for bug reports but please respect the following restrictions:
10 |
11 | * Please **try not to** use the issue tracker for personal support requests (use [Gitter](https://gitter.im/clauderic/react-infinite-calendar)).
12 | * Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others.
13 |
14 | ## Feature requests
15 |
16 | Feature requests are welcome.
17 | But take a moment to find out whether your idea fits with the scope and aims of the project.
18 | It's up to *you* to make a strong case to convince the project's developers of the merits of this feature.
19 | Please provide as much detail and context as possible.
20 |
21 | ## Pull requests
22 |
23 | Good pull requests - patches, improvements, new features - are a fantastic help.
24 | They should remain focused in scope and avoid containing unrelated commits.
25 |
26 | **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language),
27 | otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project.
28 |
29 | Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.)
30 |
31 | ## Prerequisites
32 |
33 | [Node.js](http://nodejs.org/) >= v4 must be installed.
34 |
35 | ## Installation
36 |
37 | - Running `npm install` in the components's root directory will install everything you need for development.
38 |
39 | ## Demo and Storybook
40 |
41 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
42 |
43 | - `npm run storybook` will run the storybook showcasing different states of the app at [http://localhost:9001](http://localhost:9001) with hot module reloading.
44 |
45 |
46 | ## Building
47 |
48 | - `npm run build` will build the component for publishing to npm and also bundle the demo app.
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016, Claudéric Demers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # React Infinite Calendar
6 | **Currently looking for maintainers to help maintain this project, please reach out if you would be interested**
7 |
8 | [](https://www.npmjs.com/package/react-infinite-calendar)
9 | [](https://www.npmjs.com/package/react-infinite-calendar)
10 | [](https://github.com/clauderic/react-infinite-calendar/blob/master/LICENSE)
11 | [](https://gitter.im/clauderic/react-infinite-calendar)
12 | ### Examples available here: http://clauderic.github.io/react-infinite-calendar/
13 |
14 | Features
15 | ---------------
16 |
17 | * **Infinite scroll** – Just keep scrollin', just keep scrollin'
18 | * **Flexible** – Min/max date, disabled dates, disabled days, etc.
19 | * **Extensible** – Add date range-selection, multiple date selection, or create your own HOC!
20 | * **Localization and translation** – En français, s'il vous plaît!
21 | * **Customizeable** – Customize and theme to your heart's content.
22 | * **Year selection** – For rapidly jumping from year to year
23 | * **Keyboard support** – ⬆️ ⬇️ ⬆️ ⬇️ ⬅️ ➡️ ⬅️ ➡️ ↩️
24 | * **Events and callbacks** – beforeSelect, onSelect, onScroll, etc.
25 | * **Mobile-friendly** – Silky smooth scrolling on mobile
26 |
27 |
28 |
29 |
30 |
31 | Getting Started
32 | ---------------
33 |
34 | Using [npm](https://www.npmjs.com/):
35 | ```
36 | npm install react-infinite-calendar --save
37 | ```
38 |
39 | ES6, CommonJS, and UMD builds are available with each distribution. For example:
40 | ```js
41 | import InfiniteCalendar from 'react-infinite-calendar';
42 | import 'react-infinite-calendar/styles.css'; // Make sure to import the default stylesheet
43 | ```
44 |
45 | You can also use a global-friendly UMD build:
46 | ```html
47 |
48 |
49 |
53 | ```
54 |
55 | Usage
56 | ------------
57 | ### Basic Example
58 |
59 | ```js
60 | import React from 'react';
61 | import { render } from 'react-dom';
62 | import InfiniteCalendar from 'react-infinite-calendar';
63 | import 'react-infinite-calendar/styles.css'; // only needs to be imported once
64 |
65 | // Render the Calendar
66 | var today = new Date();
67 | var lastWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7);
68 |
69 | render(
70 | ,
77 | document.getElementById('root')
78 | );
79 | ```
80 | For more usage examples, see [http://clauderic.github.io/react-infinite-calendar/](http://clauderic.github.io/react-infinite-calendar/) or check out some [code examples](https://github.com/clauderic/react-infinite-calendar/tree/master/examples).
81 |
82 | ### Prop Types
83 | | Property | Type | Default | Description |
84 | |:---------------|:----------------|:-------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
85 | | selected | Date or Boolean | `new Date()` | Value of the date that appears to be selected. Set to `false` if you don't wish to have a date initially selected. |
86 | | width | Number | `400` | Width of the calendar, in pixels |
87 | | height | Number | `600` | Height of the calendar, in pixels |
88 | | min | Date | `new Date(1980, 0, 1)` | The minimum month that can be scrolled to. |
89 | | max | Date | `new Date(2050, 11, 31)` | The maximum month that can be scrolled to. |
90 | | minDate | Date | `new Date(1980, 0, 1)` | The minimum date that is selectable. |
91 | | maxDate | Date | `new Date(2050, 11, 31)` | The maximum date that is selectable. |
92 | | disabledDays | Array | | Array of days of the week that should be disabled. For example, to disable Monday and Sunday: `[0, 6]` |
93 | | disabledDates | Array | | Array of dates that should be disabled. For example: `[new Date(2017, 1, 8), new Date(), new Date(2017, 5, 17)]` |
94 | | display | String | `'days'` | Whether to display the `years` or `days` view. |
95 | | displayOptions | Object | See [default displayOptions](https://github.com/clauderic/react-infinite-calendar/blob/master/src/utils/defaultDisplayOptions.js) | See [display options](#display-options) section for more details. |
96 | | locale | Object | See [default locale](https://github.com/clauderic/react-infinite-calendar/blob/master/src/utils/defaultLocale.js) | By default, React Infinite Calendar comes with the `English` locale. You can use this to change the language, or change the first day of the week. See [date-fns documentation](https://date-fns.org/docs/I18n) for more details |
97 | | theme | Object | See [default theme](https://github.com/clauderic/react-infinite-calendar/blob/master/src/utils/defaultTheme.js) | Basic customization of the colors |
98 | | className | String | | Optional CSS class name to append to the root `InfiniteCalendar` element. |
99 | | onSelect | Function | | Callback invoked after beforeSelect() returns true, but before the state of the calendar updates |
100 | | onScroll | Function | | Callback invoked when the scroll offset changes. `function (scrollTop: number) {}` |
101 | | onScrollEnd | Function | | Callback invoked `150ms` after the last onScroll event is triggered. `function (scrollTop: number) {}` |
102 | | rowHeight | Number | `56` | Height of each row in the calendar (each week is considered a `row`) |
103 | | autoFocus | Boolean | `true` | Whether the Calendar root should be auto-focused when it mounts. This is useful when `keyboardSupport` is enabled (the calendar must be focused to listen for keyboard events) |
104 | | tabIndex | Number | `1` | Tab-index of the calendar |
105 |
106 | ### Display Options
107 | | Property | Type | Default | Description |
108 | |:---------------------|:--------|:-------------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
109 | | layout | String | `'portrait'` | Layout of the calendar. Should be one of `'portrait'` or `'landscape'` |
110 | | showHeader | Boolean | `true` | Show/hide the header |
111 | | shouldHeaderAnimate | Boolean | `true` | Enable/Disable the header animation |
112 | | showOverlay | Boolean | `true` | Show/hide the month overlay when scrolling |
113 | | showTodayHelper | Boolean | `true` | Show/hide the floating back to `Today` helper |
114 | | showWeekdays | Boolean | `true` | Show/hide the weekdays in the header |
115 | | hideYearsOnSelect | Boolean | `true` | Whether to automatically hide the `years` view on select. |
116 | | overscanMonthCount | Number | `4` | Number of months to render above/below the visible months. Tweaking this can help reduce flickering during scrolling on certain browers/devices. |
117 | | todayHelperRowOffset | Number | `4` | This controls the number of rows to scroll past before the *Today* helper appears |
118 |
119 | Example usage of display options:
120 | ```jsx
121 |
128 | ```
129 |
130 | Dependencies
131 | ------------
132 | React Infinite Calendar has very few dependencies. It relies on [`react-tiny-virtual-list`](https://github.com/clauderic/react-tiny-virtual-list) for virtualization and [`date-fns`](https://github.com/date-fns/date-fns) for handling date manipulation. It also uses [recompose](https://github.com/acdlite/recompose) for extending the default functionality. It also has the following peerDependencies: [`react`](https://www.npmjs.com/package/react), and [`react-transition-group`](https://www.npmjs.com/package/react-transition-group).
133 |
134 | Reporting Issues
135 | ----------------
136 | If you find an [issue](https://github.com/clauderic/react-infinite-calendar/issues), please report it along with any relevant details to reproduce it. The easiest way to do so is to [fork this sandbox on CodeSandbox](https://codesandbox.io/s/zroj1zp7v4).
137 |
138 | Contributions
139 | ------------
140 | Yes please! Feature requests / pull requests are welcome. [Learn how to contribute](https://github.com/clauderic/react-infinite-calendar/blob/master/CONTRIBUTING.md)
141 |
142 | Have a suggestion or just want to say hello? Come chat on [Gitter](https://gitter.im/clauderic/react-infinite-calendar)!
143 |
144 | License
145 | ---------
146 | *react-infinite-calendar* is available under the MIT License.
147 |
--------------------------------------------------------------------------------
/demo/src/demo.css:
--------------------------------------------------------------------------------
1 |
2 | html, body, #demo {
3 | width: 100%;
4 | height: 100%;
5 | margin: 0;
6 | }
7 | #demo {
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | background: #f3f3f3;
12 | }
13 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import InfiniteCalendar from '../../src';
4 | import '../../styles.css';
5 | import './demo.css';
6 |
7 | render(
8 |
11 | , document.querySelector('#demo'));
12 |
--------------------------------------------------------------------------------
/examples/basic-setup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import InfiniteCalendar from 'react-infinite-calendar';
4 | import 'react-infinite-calendar/styles.css';
5 |
6 | // All props are optional, so this is the minimum setup you need
7 | render( , document.querySelector('#root'));
8 |
--------------------------------------------------------------------------------
/examples/keyboard-support.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import InfiniteCalendar, {
4 | Calendar,
5 | withDateSelection,
6 | withKeyboardSupport,
7 | } from 'react-infinite-calendar';
8 | import 'react-infinite-calendar/styles.css';
9 |
10 | render(
11 | ,
20 | document.querySelector('#root')
21 | );
22 |
--------------------------------------------------------------------------------
/examples/localization.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import InfiniteCalendar from 'react-infinite-calendar';
4 | import 'react-infinite-calendar/styles.css';
5 |
6 | const locale = {
7 | blank: 'Aucune date sélectionnée',
8 | headerFormat: 'dddd, D MMM',
9 | locale: require('date-fns/locale/fr'), // You need to pass in the date-fns locale for the language you want (unless it's EN)
10 | todayLabel: {
11 | long: "Aujourd'hui",
12 | short: 'Auj.',
13 | },
14 | weekdays: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
15 | weekStartsOn: 1, // Start the week on Monday
16 | };
17 |
18 | // All props are optional, so this is the minimum setup you need
19 | render( , document.querySelector('#root'));
20 |
--------------------------------------------------------------------------------
/examples/range-selection.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import InfiniteCalendar, {
4 | Calendar,
5 | withRange,
6 | } from 'react-infinite-calendar';
7 | import 'react-infinite-calendar/styles.css';
8 |
9 | const CalendarWithRange = withRange(Calendar);
10 |
11 | render(
12 | ,
22 | document.querySelector('#root')
23 | );
24 |
--------------------------------------------------------------------------------
/examples/with-multiple-dates.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import InfiniteCalendar, {
4 | Calendar,
5 | defaultMultipleDateInterpolation,
6 | withMultipleDates,
7 | } from 'react-infinite-calendar';
8 | import 'react-infinite-calendar/styles.css';
9 |
10 | const MultipleDatesCalendar = withMultipleDates(Calendar);
11 |
12 | render(
13 | ,
25 | document.querySelector('#root')
26 | );
27 |
--------------------------------------------------------------------------------
/nwb.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | module.exports = {
4 | type: 'react-component',
5 | npm: {
6 | esModules: true,
7 | umd: {
8 | global: 'InfiniteCalendar',
9 | externals: {
10 | react: 'React',
11 | 'react-dom': 'ReactDOM',
12 | 'react-addons-css-transition-group': 'ReactCSSTransitionGroup'
13 | }
14 | }
15 | },
16 | babel: {
17 | cherryPick: ['recompose'],
18 | plugins: [
19 | ['css-modules-transform', {
20 | generateScopedName: 'Cal__[name]__[local]',
21 | "preprocessCss": "./preprocess-css.js",
22 | "extensions": [".scss"],
23 | "extractCss": "./styles.css"
24 | }]
25 | ]
26 | },
27 | webpack: {
28 | rules: {
29 | 'sass-css': {
30 | modules: true,
31 | localIdentName: 'Cal__[name]__[local]',
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-infinite-calendar",
3 | "version": "2.3.1",
4 | "description": "Infinite scrolling date-picker built with React, with localization, themes, keyboard support, and more.",
5 | "main": "lib/index.js",
6 | "module": "es/index.js",
7 | "files": [
8 | "css",
9 | "es",
10 | "lib",
11 | "umd",
12 | "styles.css"
13 | ],
14 | "scripts": {
15 | "build": "npm run clean && nwb build-react-component --no-vendor",
16 | "clean": "nwb clean-module && nwb clean-demo",
17 | "start": "nwb serve-react-demo",
18 | "storybook": "start-storybook -p 9001 -c .storybook",
19 | "test": "nwb test-react",
20 | "test:coverage": "nwb test-react --coverage",
21 | "test:watch": "nwb test-react --server",
22 | "lint": "eslint --fix src/**/*.js"
23 | },
24 | "dependencies": {
25 | "classnames": "^2.2.5",
26 | "date-fns": "^1.27.2",
27 | "dom-helpers": "^3.2.1",
28 | "prop-types": "^15.5.7",
29 | "react-tiny-virtual-list": "^2.0.0",
30 | "react-transition-group": "^1.1.3",
31 | "recompose": "^0.22.0"
32 | },
33 | "peerDependencies": {
34 | "react": "^15.3.0 || ^16.0.0-alpha"
35 | },
36 | "devDependencies": {
37 | "@kadira/storybook": "^2.35.3",
38 | "@kadira/storybook-addon-options": "^1.0.1",
39 | "babel-eslint": "^7.0.0",
40 | "babel-plugin-css-modules-transform": "^1.2.1",
41 | "eslint": "^3.8.1",
42 | "eslint-config-react-app": "^0.5.1",
43 | "eslint-plugin-flowtype": "^2.21.0",
44 | "eslint-plugin-import": "^2.0.1",
45 | "eslint-plugin-jsx-a11y": "^2.2.3",
46 | "eslint-plugin-react": "^6.4.1",
47 | "node-sass": "^4.5.0",
48 | "nwb": "^0.15.6",
49 | "nwb-sass": "^0.7.1",
50 | "react": "^15.4.2",
51 | "react-dom": "^15.4.2"
52 | },
53 | "author": {
54 | "name": "Clauderic Demers",
55 | "email": "me@ced.io"
56 | },
57 | "user": "clauderic",
58 | "homepage": "https://github.com/clauderic/react-infinite-calendar",
59 | "license": "MIT",
60 | "repository": {
61 | "type": "git",
62 | "url": "https://github.com/clauderic/react-infinite-calendar.git"
63 | },
64 | "bugs": {
65 | "url": "https://github.com/clauderic/react-infinite-calendar/issues"
66 | },
67 | "keywords": [
68 | "react",
69 | "reactjs",
70 | "react-component",
71 | "infinite",
72 | "calendar",
73 | "endless",
74 | "date",
75 | "date-picker",
76 | "month",
77 | "day",
78 | "year",
79 | "scrolling",
80 | "virtual",
81 | "picker",
82 | "material",
83 | "flat"
84 | ]
85 | }
86 |
--------------------------------------------------------------------------------
/preprocess-css.js:
--------------------------------------------------------------------------------
1 | var sass = require('node-sass');
2 |
3 | module.exports = function processSass(data, filename) {
4 | var result;
5 | result = sass.renderSync({
6 | data: data,
7 | file: filename
8 | }).css;
9 | return result.toString('utf8');
10 | };
11 |
--------------------------------------------------------------------------------
/src/.stories/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable sort-keys */
2 | import React from 'react';
3 | import {addDecorator, storiesOf} from '@kadira/storybook';
4 | import InfiniteCalendar, {
5 | Calendar,
6 | defaultMultipleDateInterpolation,
7 | withDateSelection,
8 | withKeyboardSupport,
9 | withMultipleDates,
10 | withRange,
11 | } from '../';
12 | import styles from './stories.scss';
13 |
14 | // Date manipulation utils
15 | import addDays from 'date-fns/add_days';
16 | import addMonths from 'date-fns/add_months';
17 | import endOfMonth from 'date-fns/end_of_month';
18 | import format from 'date-fns/format';
19 | import isBefore from 'date-fns/is_before';
20 | import subMonths from 'date-fns/sub_months';
21 |
22 | const CenterDecorator = story => {story()}
;
23 | addDecorator(CenterDecorator);
24 |
25 | const today = new Date();
26 |
27 | storiesOf('Basic settings', module)
28 | .add('Default Configuration', () => )
29 | .add('Initially Selected Date', () => )
30 | .add('Blank Initial State', () => )
31 | .add('Min Date', () => (
32 |
37 | ))
38 | .add('Max Date', () => (
39 |
43 | ))
44 | .add('Disable Specific Dates', () => (
45 |
47 | addDays(today, amount)
48 | )}
49 | />
50 | ))
51 | .add('Disable Specific Weekdays', () => (
52 |
53 | ));
54 |
55 | storiesOf('Higher Order Components', module)
56 | .add('Range selection', () => (
57 |
67 | ))
68 | .add('Multiple date selection', () => {
69 | return (
70 |
75 | );
76 | })
77 | .add('Keyboard Support', () => {
78 | return ;
79 | });
80 |
81 | storiesOf('Internationalization', module)
82 | .add('Locale', () => (
83 |
95 | ))
96 | .add('First Day of the Week', () => (
97 |
102 | ));
103 |
104 | storiesOf('Customization', module)
105 | .add('Theming', () => (
106 |
122 | ))
123 | .add('Flexible Size', () => (
124 |
129 | ))
130 | .add('Select Year First', () => (
131 |
132 | ))
133 | .add('Dynamic Selection Color', () => (
134 | {
138 | return isBefore(date, today) ? '#EC6150' : '#559FFF';
139 | },
140 | }}
141 | />
142 | ));
143 |
144 | storiesOf('Display Options', module)
145 | .add('Landscape Layout', () => (
146 |
153 | ))
154 | .add('Disable Header', () => (
155 |
160 | ))
161 | .add('Disable Header Animation', () => (
162 |
167 | ))
168 | .add('Disable Month Overlay', () => (
169 |
174 | ))
175 | .add('Disable Floating Today Helper', () => (
176 |
181 | ))
182 | .add('Hide Months in Year Selection', () => (
183 |
189 | ))
190 | .add('Hide Weekdays Helper', () => (
191 |
196 | ));
197 |
198 | storiesOf('Events', module)
199 | .add('On Select', () => (
200 |
202 | alert(`You selected: ${format(date, 'ddd, MMM Do YYYY')}`)}
203 | />
204 | ))
205 | .add('On Scroll', () => [
206 | Check your console logs. ,
207 |
210 | console.info('onScroll() – Scroll top:', scrollTop)}
211 | />,
212 | ]);
213 |
--------------------------------------------------------------------------------
/src/.stories/stories.scss:
--------------------------------------------------------------------------------
1 | .center {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: center;
8 | background: #f3f3f3;
9 |
10 | > label {
11 | display: block;
12 | margin: 0 auto;
13 | margin-bottom: 20px;
14 | text-align: center;
15 | font-family: Helvetica Neue,Helvetica,Arial,sans-serif;
16 | color: #888;
17 | -webkit-font-smoothing: antialiased;
18 | font-size: 15px;
19 | line-height: 1.4;
20 | font-style: normal;
21 |
22 | code {
23 | background: #eaeaea;
24 | padding: 2px 7px;
25 | font-size: 14px;
26 | border-radius: 2px;
27 | }
28 | }
29 | }
30 |
31 | :global {
32 | html, body, #root {
33 | width: 100%;
34 | height: 100%;
35 | margin: 0;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Calendar/Container.scss:
--------------------------------------------------------------------------------
1 | @import "../variables";
2 |
3 | .root {
4 | position: relative;
5 | display: table;
6 | z-index: 1;
7 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
8 | line-height: 1.4em;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | font-weight: 300;
12 | outline: none;
13 | text-align: left;
14 |
15 | &.landscape {
16 | display: flex;
17 | flex-direction: row;
18 |
19 | .wrapper {
20 | position: relative;
21 | flex-grow: 1;
22 | overflow: hidden;
23 | z-index: 1;
24 | border-top-right-radius: $borderRadius;
25 | border-bottom-right-radius: $borderRadius;
26 | }
27 | }
28 | }
29 | .listWrapper {
30 | position: relative;
31 | overflow: hidden;
32 | background-color: #FFF;
33 | }
34 |
--------------------------------------------------------------------------------
/src/Calendar/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 | import {debounce, emptyFn, range, ScrollSpeed} from '../utils';
5 | import {defaultProps} from 'recompose';
6 | import defaultDisplayOptions from '../utils/defaultDisplayOptions';
7 | import defaultLocale from '../utils/defaultLocale';
8 | import defaultTheme from '../utils/defaultTheme';
9 | import Today, {DIRECTION_UP, DIRECTION_DOWN} from '../Today';
10 | import Header from '../Header';
11 | import MonthList from '../MonthList';
12 | import Weekdays from '../Weekdays';
13 | import Years from '../Years';
14 | import Day from '../Day';
15 | import parse from 'date-fns/parse';
16 | import format from 'date-fns/format';
17 | import startOfDay from 'date-fns/start_of_day';
18 |
19 | const styles = {
20 | container: require('./Container.scss'),
21 | day: require('../Day/Day.scss'),
22 | };
23 |
24 | export const withDefaultProps = defaultProps({
25 | autoFocus: true,
26 | DayComponent: Day,
27 | display: 'days',
28 | displayOptions: {},
29 | HeaderComponent: Header,
30 | height: 500,
31 | keyboardSupport: true,
32 | max: new Date(2050, 11, 31),
33 | maxDate: new Date(2050, 11, 31),
34 | min: new Date(1980, 0, 1),
35 | minDate: new Date(1980, 0, 1),
36 | onHighlightedDateChange: emptyFn,
37 | onScroll: emptyFn,
38 | onScrollEnd: emptyFn,
39 | onSelect: emptyFn,
40 | passThrough: {},
41 | rowHeight: 56,
42 | tabIndex: 1,
43 | width: 400,
44 | YearsComponent: Years,
45 | });
46 |
47 | export default class Calendar extends Component {
48 | constructor(props) {
49 | super(...arguments);
50 |
51 | this.updateYears(props);
52 |
53 | this.state = {
54 | display: props.display,
55 | };
56 | }
57 | static propTypes = {
58 | autoFocus: PropTypes.bool,
59 | className: PropTypes.string,
60 | DayComponent: PropTypes.func,
61 | disabledDates: PropTypes.arrayOf(PropTypes.instanceOf(Date)),
62 | disabledDays: PropTypes.arrayOf(PropTypes.number),
63 | display: PropTypes.oneOf(['years', 'days']),
64 | displayOptions: PropTypes.shape({
65 | hideYearsOnSelect: PropTypes.bool,
66 | layout: PropTypes.oneOf(['portrait', 'landscape']),
67 | overscanMonthCount: PropTypes.number,
68 | shouldHeaderAnimate: PropTypes.bool,
69 | showHeader: PropTypes.bool,
70 | showMonthsForYears: PropTypes.bool,
71 | showOverlay: PropTypes.bool,
72 | showTodayHelper: PropTypes.bool,
73 | showWeekdays: PropTypes.bool,
74 | todayHelperRowOffset: PropTypes.number,
75 | }),
76 | height: PropTypes.number,
77 | keyboardSupport: PropTypes.bool,
78 | locale: PropTypes.shape({
79 | blank: PropTypes.string,
80 | headerFormat: PropTypes.string,
81 | todayLabel: PropTypes.shape({
82 | long: PropTypes.string,
83 | short: PropTypes.string,
84 | }),
85 | weekdays: PropTypes.arrayOf(PropTypes.string),
86 | weekStartsOn: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6]),
87 | }),
88 | max: PropTypes.instanceOf(Date),
89 | maxDate: PropTypes.instanceOf(Date),
90 | min: PropTypes.instanceOf(Date),
91 | minDate: PropTypes.instanceOf(Date),
92 | onScroll: PropTypes.func,
93 | onScrollEnd: PropTypes.func,
94 | onSelect: PropTypes.func,
95 | rowHeight: PropTypes.number,
96 | tabIndex: PropTypes.number,
97 | theme: PropTypes.shape({
98 | floatingNav: PropTypes.shape({
99 | background: PropTypes.string,
100 | chevron: PropTypes.string,
101 | color: PropTypes.string,
102 | }),
103 | headerColor: PropTypes.string,
104 | selectionColor: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
105 | textColor: PropTypes.shape({
106 | active: PropTypes.string,
107 | default: PropTypes.string,
108 | }),
109 | todayColor: PropTypes.string,
110 | weekdayColor: PropTypes.string,
111 | }),
112 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
113 | YearsComponent: PropTypes.func,
114 | };
115 | componentDidMount() {
116 | let {autoFocus} = this.props;
117 |
118 | if (autoFocus) {
119 | this.node.focus();
120 | }
121 | }
122 | componentWillUpdate(nextProps, nextState) {
123 | let {min, minDate, max, maxDate} = this.props;
124 |
125 | if (nextProps.min !== min || nextProps.minDate !== minDate || nextProps.max !== max || nextProps.maxDate !== maxDate) {
126 | this.updateYears(nextProps);
127 | }
128 |
129 | if (nextProps.display !== this.props.display) {
130 | this.setState({display: nextProps.display});
131 | }
132 | }
133 | updateYears(props = this.props) {
134 | this._min = parse(props.min);
135 | this._max = parse(props.max);
136 | this._minDate = parse(props.minDate);
137 | this._maxDate = parse(props.maxDate);
138 |
139 | const min = this._min.getFullYear();
140 | const minMonth = this._min.getMonth();
141 | const max = this._max.getFullYear();
142 | const maxMonth = this._max.getMonth();
143 |
144 | const months = [];
145 | let year, month;
146 | for (year = min; year <= max; year++) {
147 | for (month = 0; month < 12; month++) {
148 | if (
149 | year === min && month < minMonth ||
150 | year === max && month > maxMonth
151 | ) {
152 | continue;
153 | }
154 |
155 | months.push({month, year});
156 | }
157 | }
158 |
159 | this.months = months;
160 | }
161 | getDisabledDates(disabledDates) {
162 | return disabledDates && disabledDates.map((date) => format(parse(date), 'YYYY-MM-DD'));
163 | }
164 | _displayOptions = {};
165 | getDisplayOptions(displayOptions = this.props.displayOptions) {
166 | return Object.assign(this._displayOptions, defaultDisplayOptions, displayOptions);
167 | }
168 | _locale = {};
169 | getLocale() {
170 | return Object.assign(this._locale, defaultLocale, this.props.locale);
171 | }
172 | _theme = {};
173 | getTheme() {
174 | return Object.assign(this._theme, defaultTheme, this.props.theme);
175 | }
176 | getCurrentOffset = () => {
177 | return this.scrollTop;
178 | }
179 | getDateOffset = (date) => {
180 | return this._MonthList && this._MonthList.getDateOffset(date);
181 | };
182 | scrollTo = (offset) => {
183 | return this._MonthList && this._MonthList.scrollTo(offset);
184 | }
185 | scrollToDate = (date = new Date(), offset, shouldAnimate) => {
186 | const {display} = this.props;
187 |
188 | return this._MonthList &&
189 | this._MonthList.scrollToDate(
190 | date,
191 | offset,
192 | shouldAnimate && display === 'days',
193 | () => this.setState({isScrolling: false}),
194 | );
195 | };
196 | getScrollSpeed = new ScrollSpeed().getScrollSpeed;
197 | handleScroll = (scrollTop, e) => {
198 | const {onScroll, rowHeight} = this.props;
199 | const {isScrolling} = this.state;
200 | const {showTodayHelper, showOverlay} = this.getDisplayOptions();
201 | const scrollSpeed = this.scrollSpeed = Math.abs(this.getScrollSpeed(scrollTop));
202 | this.scrollTop = scrollTop;
203 |
204 | // We only want to display the months overlay if the user is rapidly scrolling
205 | if (showOverlay && scrollSpeed > rowHeight && !isScrolling) {
206 | this.setState({
207 | isScrolling: true,
208 | });
209 | }
210 |
211 | if (showTodayHelper) {
212 | this.updateTodayHelperPosition(scrollSpeed);
213 | }
214 |
215 | onScroll(scrollTop, e);
216 | this.handleScrollEnd();
217 | };
218 | handleScrollEnd = debounce(() => {
219 | const {onScrollEnd} = this.props;
220 | const {isScrolling} = this.state;
221 | const {showTodayHelper} = this.getDisplayOptions();
222 |
223 | if (isScrolling) {
224 | this.setState({isScrolling: false});
225 | }
226 |
227 | if (showTodayHelper) {
228 | this.updateTodayHelperPosition(0);
229 | }
230 |
231 | onScrollEnd(this.scrollTop);
232 | }, 150);
233 | updateTodayHelperPosition = (scrollSpeed) => {
234 | const today = this.today;
235 | const scrollTop = this.scrollTop;
236 | const {showToday} = this.state;
237 | const {height, rowHeight} = this.props;
238 | const {todayHelperRowOffset} = this.getDisplayOptions();
239 | let newState;
240 |
241 | if (!this._todayOffset) {
242 | this._todayOffset = this.getDateOffset(today);
243 | }
244 |
245 | // Today is above the fold
246 | if (scrollTop >= this._todayOffset + (height - rowHeight) / 2 + rowHeight * todayHelperRowOffset) {
247 | if (showToday !== DIRECTION_UP) newState = DIRECTION_UP;
248 | }
249 | // Today is below the fold
250 | else if (scrollTop <= this._todayOffset - height / 2 - rowHeight * (todayHelperRowOffset + 1)) {
251 | if (showToday !== DIRECTION_DOWN) newState = DIRECTION_DOWN;
252 | } else if (showToday && scrollSpeed <= 1) {
253 | newState = false;
254 | }
255 |
256 | if (scrollTop === 0) {
257 | newState = false;
258 | }
259 |
260 | if (newState != null) {
261 | this.setState({showToday: newState});
262 | }
263 | };
264 | setDisplay = (display) => {
265 | this.setState({display});
266 | }
267 | render() {
268 | let {
269 | className,
270 | passThrough,
271 | DayComponent,
272 | disabledDays,
273 | displayDate,
274 | height,
275 | HeaderComponent,
276 | rowHeight,
277 | scrollDate,
278 | selected,
279 | tabIndex,
280 | width,
281 | YearsComponent,
282 | } = this.props;
283 | const {
284 | hideYearsOnSelect,
285 | layout,
286 | overscanMonthCount,
287 | shouldHeaderAnimate,
288 | showHeader,
289 | showMonthsForYears,
290 | showOverlay,
291 | showTodayHelper,
292 | showWeekdays,
293 | } = this.getDisplayOptions();
294 | const {display, isScrolling, showToday} = this.state;
295 | const disabledDates = this.getDisabledDates(this.props.disabledDates);
296 | const locale = this.getLocale();
297 | const theme = this.getTheme();
298 | const today = this.today = startOfDay(new Date());
299 |
300 | return (
301 | {
309 | this.node = node;
310 | }}
311 | {...passThrough.rootNode}
312 | >
313 | {showHeader &&
314 |
327 | }
328 |
329 | {showWeekdays &&
330 |
331 | }
332 |
333 | {showTodayHelper &&
334 |
341 | }
342 | {
344 | this._MonthList = instance;
345 | }}
346 | DayComponent={DayComponent}
347 | disabledDates={disabledDates}
348 | disabledDays={disabledDays}
349 | height={height}
350 | isScrolling={isScrolling}
351 | locale={locale}
352 | maxDate={this._maxDate}
353 | min={this._min}
354 | minDate={this._minDate}
355 | months={this.months}
356 | onScroll={this.handleScroll}
357 | overscanMonthCount={overscanMonthCount}
358 | passThrough={passThrough}
359 | theme={theme}
360 | today={today}
361 | rowHeight={rowHeight}
362 | selected={selected}
363 | scrollDate={scrollDate}
364 | showOverlay={showOverlay}
365 | width={width}
366 | />
367 |
368 | {display === 'years' &&
369 |
{
371 | this._Years = instance;
372 | }}
373 | height={height}
374 | hideOnSelect={hideYearsOnSelect}
375 | locale={locale}
376 | max={this._max}
377 | maxDate={this._maxDate}
378 | min={this._min}
379 | minDate={this._minDate}
380 | scrollToDate={this.scrollToDate}
381 | selected={selected}
382 | setDisplay={this.setDisplay}
383 | showMonths={showMonthsForYears}
384 | theme={theme}
385 | today={today}
386 | width={width}
387 | years={range(this._min.getFullYear(), this._max.getFullYear() + 1)}
388 | {...passThrough.Years}
389 | />
390 | }
391 |
392 |
393 | );
394 | }
395 | };
396 |
--------------------------------------------------------------------------------
/src/Calendar/withDateSelection.js:
--------------------------------------------------------------------------------
1 | import {
2 | compose,
3 | withProps,
4 | withPropsOnChange,
5 | withState,
6 | } from 'recompose';
7 | import {withDefaultProps} from './';
8 | import {sanitizeDate, withImmutableProps} from '../utils';
9 | import format from 'date-fns/format';
10 | import parse from 'date-fns/parse';
11 |
12 | export const enhanceDay = withPropsOnChange(['selected'], props => ({
13 | isSelected: props.selected === props.date,
14 | }));
15 |
16 | const enhanceYear = withPropsOnChange(['selected'], ({selected}) => ({
17 | selected: parse(selected),
18 | }));
19 |
20 | // Enhancer to handle selecting and displaying a single date
21 | export const withDateSelection = compose(
22 | withDefaultProps,
23 | withImmutableProps(({
24 | DayComponent,
25 | onSelect,
26 | setScrollDate,
27 | YearsComponent,
28 | }) => ({
29 | DayComponent: enhanceDay(DayComponent),
30 | YearsComponent: enhanceYear(YearsComponent),
31 | })),
32 | withState('scrollDate', 'setScrollDate', props => props.selected || new Date()),
33 | withProps(({onSelect, setScrollDate, ...props}) => {
34 | const selected = sanitizeDate(props.selected, props);
35 |
36 | return {
37 | passThrough: {
38 | Day: {
39 | onClick: onSelect,
40 | },
41 | Years: {
42 | onSelect: (year) => handleYearSelect(year, {onSelect, selected, setScrollDate}),
43 | },
44 | },
45 | selected: selected && format(selected, 'YYYY-MM-DD'),
46 | };
47 | }),
48 | );
49 |
50 | function handleYearSelect(date, {setScrollDate, selected, onSelect}) {
51 | const newDate = parse(date);
52 |
53 | onSelect(newDate);
54 | setScrollDate(newDate);
55 | }
56 |
--------------------------------------------------------------------------------
/src/Calendar/withKeyboardSupport.js:
--------------------------------------------------------------------------------
1 | import {
2 | compose,
3 | withHandlers,
4 | withProps,
5 | withState,
6 | } from 'recompose';
7 | import addDays from 'date-fns/add_days';
8 | import format from 'date-fns/format';
9 | import isAfter from 'date-fns/is_after';
10 | import isBefore from 'date-fns/is_before';
11 | import {keyCodes, withImmutableProps} from '../utils';
12 |
13 | const enhanceDay = withProps(props => ({
14 | isHighlighted: props.highlightedDate === props.date,
15 | }));
16 |
17 | export const withKeyboardSupport = compose(
18 | withState('highlightedDate', 'setHighlight'),
19 | withImmutableProps(({DayComponent}) => ({
20 | DayComponent: enhanceDay(DayComponent),
21 | })),
22 | withHandlers({
23 | onKeyDown: props => e => handleKeyDown(e, props),
24 | }),
25 | withProps(({highlightedDate, onKeyDown, onSelect, passThrough, setHighlight}) => ({
26 | passThrough: {
27 | ...passThrough,
28 | Day: {
29 | ...passThrough.Day,
30 | highlightedDate: format(highlightedDate, 'YYYY-MM-DD'),
31 | onClick: (date) => {
32 | setHighlight(null);
33 | passThrough.Day.onClick(date);
34 | },
35 | },
36 | rootNode: {onKeyDown},
37 | },
38 | })),
39 | );
40 |
41 | function handleKeyDown(e, props) {
42 | const {
43 | minDate,
44 | maxDate,
45 | passThrough: {Day: {onClick}},
46 | setScrollDate,
47 | setHighlight,
48 | } = props;
49 | const highlightedDate = getInitialDate(props);
50 | let delta = 0;
51 |
52 | if (
53 | [keyCodes.left, keyCodes.up, keyCodes.right, keyCodes.down].indexOf(
54 | e.keyCode,
55 | ) > -1 &&
56 | typeof e.preventDefault === 'function'
57 | ) {
58 | e.preventDefault();
59 | }
60 |
61 | switch (e.keyCode) {
62 | case keyCodes.enter:
63 | onClick && onClick(highlightedDate);
64 | return;
65 | case keyCodes.left:
66 | delta = -1;
67 | break;
68 | case keyCodes.right:
69 | delta = +1;
70 | break;
71 | case keyCodes.down:
72 | delta = +7;
73 | break;
74 | case keyCodes.up:
75 | delta = -7;
76 | break;
77 | default:
78 | delta = 0;
79 | }
80 |
81 | if (delta) {
82 | let newHighlightedDate = addDays(highlightedDate, delta);
83 |
84 | // Make sure the new highlighted date isn't before min / max
85 | if (isBefore(newHighlightedDate, minDate)) {
86 | newHighlightedDate = new Date(minDate);
87 | } else if (isAfter(newHighlightedDate, maxDate)) {
88 | newHighlightedDate = new Date(maxDate);
89 | }
90 |
91 | setScrollDate(newHighlightedDate);
92 | setHighlight(newHighlightedDate);
93 | }
94 | }
95 |
96 | function getInitialDate({highlightedDate, selected, displayDate}) {
97 | return highlightedDate || selected.start || displayDate || selected || new Date();
98 | }
99 |
--------------------------------------------------------------------------------
/src/Calendar/withMultipleDates.js:
--------------------------------------------------------------------------------
1 | import {compose, withProps, withPropsOnChange, withState} from 'recompose';
2 | import {withDefaultProps} from './';
3 | import {sanitizeDate, withImmutableProps} from '../utils';
4 | import enhanceHeader from '../Header/withMultipleDates';
5 | import format from 'date-fns/format';
6 | import parse from 'date-fns/parse';
7 |
8 | // Enhance Day component to display selected state based on an array of selected dates
9 | export const enhanceDay = withPropsOnChange(['selected'], props => ({
10 | isSelected: props.selected.indexOf(props.date) !== -1,
11 | }));
12 |
13 | // Enhance year component
14 | const enhanceYears = withProps(({displayDate}) => ({
15 | selected: displayDate ? parse(displayDate) : null,
16 | }));
17 |
18 | // Enhancer to handle selecting and displaying multiple dates
19 | export const withMultipleDates = compose(
20 | withDefaultProps,
21 | withState('scrollDate', 'setScrollDate', getInitialDate),
22 | withState('displayDate', 'setDisplayDate', getInitialDate),
23 | withImmutableProps(({
24 | DayComponent,
25 | HeaderComponent,
26 | YearsComponent,
27 | }) => ({
28 | DayComponent: enhanceDay(DayComponent),
29 | HeaderComponent: enhanceHeader(HeaderComponent),
30 | YearsComponent: enhanceYears(YearsComponent),
31 | })),
32 | withProps(({displayDate, onSelect, setDisplayDate, scrollToDate, ...props}) => ({
33 | passThrough: {
34 | Day: {
35 | onClick: (date) => handleSelect(date, {onSelect, setDisplayDate}),
36 | },
37 | Header: {
38 | setDisplayDate,
39 | },
40 | Years: {
41 | displayDate,
42 | onSelect: (year, e, callback) => handleYearSelect(year, callback),
43 | selected: displayDate,
44 | },
45 | },
46 | selected: props.selected
47 | .filter(date => sanitizeDate(date, props))
48 | .map(date => format(date, 'YYYY-MM-DD')),
49 | })),
50 | );
51 |
52 | function handleSelect(date, {onSelect, setDisplayDate}) {
53 | onSelect(date);
54 | setDisplayDate(date);
55 | }
56 |
57 | function handleYearSelect(date, callback) {
58 | callback(parse(date));
59 | }
60 |
61 | function getInitialDate({selected}) {
62 | return selected.length ? selected[0] : new Date();
63 | }
64 |
65 | export function defaultMultipleDateInterpolation(date, selected) {
66 | const selectedMap = selected.map(date => format(date, 'YYYY-MM-DD'));
67 | const index = selectedMap.indexOf(format(date, 'YYYY-MM-DD'));
68 |
69 | return (index === -1)
70 | ? [...selected, date]
71 | : [...selected.slice(0, index), ...selected.slice(index+1)];
72 | }
73 |
--------------------------------------------------------------------------------
/src/Calendar/withRange.js:
--------------------------------------------------------------------------------
1 | import {compose, withProps, withPropsOnChange, withState} from 'recompose';
2 | import classNames from 'classnames';
3 | import {withDefaultProps} from './';
4 | import {withImmutableProps} from '../utils';
5 | import isBefore from 'date-fns/is_before';
6 | import enhanceHeader from '../Header/withRange';
7 | import format from 'date-fns/format';
8 | import parse from 'date-fns/parse';
9 | import styles from '../Day/Day.scss';
10 |
11 | let isTouchDevice = false;
12 |
13 | export const EVENT_TYPE = {
14 | END: 3,
15 | HOVER: 2,
16 | START: 1,
17 | };
18 |
19 | // Enhance Day component to display selected state based on an array of selected dates
20 | export const enhanceDay = withPropsOnChange(['selected'], ({date, selected, theme}) => {
21 | const isSelected = date >= selected.start && date <= selected.end;
22 | const isStart = date === selected.start;
23 | const isEnd = date === selected.end;
24 | const isRange = !(isStart && isEnd);
25 | const style = isRange && (
26 | isStart && {backgroundColor: theme.accentColor} ||
27 | isEnd && {borderColor: theme.accentColor}
28 | );
29 |
30 | return {
31 | className: isSelected && isRange && classNames(styles.range, {
32 | [styles.start]: isStart,
33 | [styles.betweenRange]: !isStart && !isEnd,
34 | [styles.end]: isEnd,
35 | }),
36 | isSelected,
37 | selectionStyle: style,
38 | };
39 | });
40 |
41 | // Enhancer to handle selecting and displaying multiple dates
42 | export const withRange = compose(
43 | withDefaultProps,
44 | withState('scrollDate', 'setScrollDate', getInitialDate),
45 | withState('displayKey', 'setDisplayKey', getInitialDate),
46 | withState('selectionStart', 'setSelectionStart', null),
47 | withImmutableProps(({
48 | DayComponent,
49 | HeaderComponent,
50 | YearsComponent,
51 | }) => ({
52 | DayComponent: enhanceDay(DayComponent),
53 | HeaderComponent: enhanceHeader(HeaderComponent),
54 | })),
55 | withProps(({displayKey, passThrough, selected, setDisplayKey, ...props}) => ({
56 | /* eslint-disable sort-keys */
57 | passThrough: {
58 | ...passThrough,
59 | Day: {
60 | onClick: (date) => handleSelect(date, {selected, ...props}),
61 | handlers: {
62 | onMouseOver: !isTouchDevice && props.selectionStart
63 | ? (e) => handleMouseOver(e, {selected, ...props})
64 | : null,
65 | },
66 | },
67 | Years: {
68 | selected: selected && selected[displayKey],
69 | onSelect: (date) => handleYearSelect(date, {displayKey, selected, ...props}),
70 | },
71 | Header: {
72 | onYearClick: (date, e, key) => setDisplayKey(key || 'start'),
73 | },
74 | },
75 | selected: {
76 | start: selected && format(selected.start, 'YYYY-MM-DD'),
77 | end: selected && format(selected.end, 'YYYY-MM-DD'),
78 | },
79 | })),
80 | );
81 |
82 | function getSortedSelection({start, end}) {
83 | return isBefore(start, end)
84 | ? {start, end}
85 | : {start: end, end: start};
86 | }
87 |
88 | function handleSelect(date, {onSelect, selected, selectionStart, setSelectionStart}) {
89 | if (selectionStart) {
90 | onSelect({
91 | eventType: EVENT_TYPE.END,
92 | ...getSortedSelection({
93 | start: selectionStart,
94 | end: date,
95 | }),
96 | });
97 | setSelectionStart(null);
98 | } else {
99 | onSelect({eventType:EVENT_TYPE.START, start: date, end: date});
100 | setSelectionStart(date);
101 | }
102 | }
103 |
104 | function handleMouseOver(e, {onSelect, selectionStart}) {
105 | const dateStr = e.target.getAttribute('data-date');
106 | const date = dateStr && parse(dateStr);
107 |
108 | if (!date) { return; }
109 |
110 | onSelect({
111 | eventType: EVENT_TYPE.HOVER,
112 | ...getSortedSelection({
113 | start: selectionStart,
114 | end: date,
115 | }),
116 | });
117 | }
118 |
119 | function handleYearSelect(date, {displayKey, onSelect, selected, setScrollDate}) {
120 |
121 | setScrollDate(date);
122 | onSelect(getSortedSelection(
123 | Object.assign({}, selected, {[displayKey]: parse(date)}))
124 | );
125 | }
126 |
127 | function getInitialDate({selected}) {
128 | return selected && selected.start || new Date();
129 | }
130 |
131 | if (typeof window !== 'undefined') {
132 | window.addEventListener('touchstart', function onTouch() {
133 | isTouchDevice = true;
134 |
135 | window.removeEventListener('touchstart', onTouch, false);
136 | });
137 | }
138 |
--------------------------------------------------------------------------------
/src/Day/Day.scss:
--------------------------------------------------------------------------------
1 | @import "../variables";
2 |
3 | @mixin circle() {
4 | content: '';
5 | position: absolute;
6 | top: 50%;
7 | left: 50%;
8 | width: $rowHeight - 4px;
9 | height: $rowHeight - 4px;
10 | margin-top: -0.5 * ($rowHeight - 4px);
11 | margin-left: -0.5 * ($rowHeight - 4px);
12 | border-radius: 50%;
13 | }
14 |
15 | .root {
16 | display: inline-block;
17 | box-sizing: border-box;
18 | width: 1 / 7 * 100%;
19 |
20 | list-style: none;
21 |
22 | font-size: 16px;
23 | text-align: center;
24 |
25 | cursor: pointer;
26 | user-select: none;
27 |
28 | &.enabled {
29 | &.highlighted, &:active, &:hover {
30 | position: relative;
31 | z-index: 1;
32 |
33 | &:before {
34 | @include circle();
35 |
36 | background-color: $cellHoverBg;
37 | z-index: -1;
38 | }
39 | }
40 |
41 | &:hover:before {
42 | opacity: 0.5;
43 | }
44 | &.highlighted:before, &:active:before {
45 | opacity: 1;
46 | }
47 | }
48 |
49 | &:first-child {
50 | position: relative;
51 | }
52 |
53 | &.today {
54 | position: relative;
55 | z-index: 2;
56 |
57 | > span {
58 | color: $textColor;
59 | }
60 |
61 | &.disabled > span {
62 | color: $textColorDisabled;
63 | }
64 |
65 | &:before {
66 | @include circle();
67 | box-shadow: inset 0 0 0 1px;
68 | z-index: -1;
69 | }
70 |
71 | &.disabled:before {
72 | box-shadow: inset 0 0 0 1px #BBB;
73 | }
74 | }
75 | &.selected {
76 | position: relative;
77 |
78 | > .month, > .year {
79 | display: none;
80 | }
81 |
82 | &:before {
83 | display: none;
84 | }
85 |
86 | .selection {
87 | @include circle();
88 | line-height: $rowHeight;
89 | z-index: 2;
90 |
91 | .month {
92 | top: 9px;
93 | }
94 | .day {
95 | position: relative;
96 | top: 5px;
97 |
98 | font-weight: bold;
99 | font-size: 18px;
100 | }
101 | }
102 | }
103 | &.disabled {
104 | color: $textColorDisabled;
105 | cursor: not-allowed;
106 | }
107 | }
108 |
109 | .month, .year {
110 | position: absolute;
111 | left: 0;
112 | right: 0;
113 |
114 | font-size: 12px;
115 | line-height: 12px;
116 | text-transform: capitalize;
117 | }
118 |
119 | .month {
120 | top: 5px;
121 | }
122 |
123 | .year {
124 | bottom: 5px;
125 | }
126 |
127 | /*
128 | * Range selection styles
129 | */
130 | .range.selected {
131 | &.start, &.end {
132 | &:after {
133 | content: '';
134 | position: absolute;
135 | top: 50%;
136 | width: 50%;
137 | height: $rowHeight - 4px;
138 | margin-top: -0.5 * ($rowHeight - 4px);
139 | box-shadow: inset $rowHeight $rowHeight;
140 | }
141 | }
142 |
143 | &.disabled {
144 | .selection.selection {
145 | background-color: #EEE !important;
146 |
147 | .day, .month {
148 | color: #AAA;
149 | font-weight: 300;
150 | }
151 | }
152 | }
153 |
154 | &.start {
155 | .selection {
156 | border-top-left-radius: 50%;
157 | border-bottom-left-radius: 50%;
158 | }
159 |
160 | &:after {
161 | right: 0;
162 | }
163 |
164 | &.end:after {
165 | display: none;
166 | }
167 | }
168 | &.betweenRange {
169 | .selection {
170 | left: 0;
171 | right: 0;
172 | width: 100%;
173 | margin-left: 0;
174 | display: flex;
175 | justify-content: center;
176 | align-items: center;
177 | border-radius: 0;
178 | }
179 | .day {
180 | top: 0;
181 | font-size: 16px;
182 | }
183 | .month {
184 | display: none;
185 | }
186 | }
187 | &.end {
188 | &:after {
189 | left: 0;
190 | }
191 |
192 | .selection {
193 | border-top-right-radius: 50%;
194 | border-bottom-right-radius: 50%;
195 |
196 | color: inherit !important;
197 | background-color: #FFF !important;
198 | border: 2px solid;
199 | box-sizing: border-box;
200 |
201 | .day {
202 | top: 4px;
203 | }
204 | }
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/Day/index.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import classNames from 'classnames';
3 | import parse from 'date-fns/parse';
4 | import styles from './Day.scss';
5 |
6 | export default class Day extends PureComponent {
7 | handleClick = () => {
8 | let {date, isDisabled, onClick} = this.props;
9 |
10 | if (!isDisabled && typeof onClick === 'function') {
11 | onClick(parse(date));
12 | }
13 | };
14 |
15 | renderSelection(selectionColor) {
16 | const {
17 | day,
18 | date,
19 | isToday,
20 | locale: {todayLabel},
21 | monthShort,
22 | theme: {textColor},
23 | selectionStyle,
24 | } = this.props;
25 |
26 | return (
27 |
36 |
37 | {isToday ? todayLabel.short || todayLabel.long : monthShort}
38 |
39 | {day}
40 |
41 | );
42 | }
43 |
44 | render() {
45 | const {
46 | className,
47 | currentYear,
48 | date,
49 | day,
50 | handlers,
51 | isDisabled,
52 | isHighlighted,
53 | isToday,
54 | isSelected,
55 | monthShort,
56 | theme: {selectionColor, todayColor},
57 | year,
58 | } = this.props;
59 | let color;
60 |
61 | if (isSelected) {
62 | color = this.selectionColor = typeof selectionColor === 'function'
63 | ? selectionColor(date)
64 | : selectionColor;
65 | } else if (isToday) {
66 | color = todayColor;
67 | }
68 |
69 | return (
70 |
83 | {day === 1 && {monthShort} }
84 | {isToday ? {day} : day}
85 | {day === 1 &&
86 | currentYear !== year &&
87 | {year} }
88 | {isSelected && this.renderSelection()}
89 |
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Header/Animation.scss:
--------------------------------------------------------------------------------
1 | .enter {
2 | opacity: 0;
3 | transform: translate3d(0, -100%, 0);
4 | transition: 0.25s ease;
5 | }
6 | .enter.enterActive {
7 | opacity: 1;
8 | transform: translate3d(0, 0, 0);
9 | }
10 | .leave {
11 | transform: translate3d(0, 0, 0);
12 | transition: 0.25s ease;
13 | }
14 | .leave.leaveActive {
15 | opacity: 0;
16 | transform: translate3d(0, 100%, 0);
17 | }
18 |
--------------------------------------------------------------------------------
/src/Header/Header.scss:
--------------------------------------------------------------------------------
1 | @import "../variables";
2 |
3 | .root {
4 | position: relative;
5 | display: flex;
6 | align-items: center;
7 | box-sizing: border-box;
8 | overflow: hidden;
9 |
10 | min-height: 98px;
11 | padding: 20px;
12 | line-height: 1.3;
13 | font-weight: 400;
14 | border-top-left-radius: $borderRadius;
15 | border-top-right-radius: $borderRadius;
16 |
17 | &.landscape {
18 | align-items: flex-start;
19 | min-width: 200px;
20 | border-top-right-radius: 0;
21 | border-bottom-left-radius: $borderRadius;
22 |
23 | .dateWrapper.day {
24 | flex-grow: 1;
25 | height: 76px;
26 | }
27 | }
28 | }
29 |
30 | .wrapper {
31 | display: flex;
32 | flex-direction: column;
33 | flex-grow: 1;
34 | cursor: pointer;
35 |
36 | &.blank {
37 | height: 58px;
38 | line-height: 58px;
39 | color: rgba(255,255,255,0.5);
40 | font-size: 18px;
41 | cursor: default;
42 | }
43 | }
44 |
45 | .dateWrapper {
46 | position: relative;
47 | display: block;
48 | overflow: hidden;
49 | color: rgba(255,255,255,0.5);
50 | transition: color 0.3s ease;
51 |
52 | &.active {
53 | color: rgb(255,255,255);
54 | }
55 | &.day {
56 | height: 38px;
57 | font-size: 36px;
58 | line-height: 36px;
59 | text-transform: capitalize;
60 | }
61 | &.year {
62 | height: 20px;
63 | font-size: 18px;
64 | line-height: 18px;
65 | }
66 | }
67 | .date {
68 | position: absolute;
69 | top: 0;
70 | left: 0;
71 | right: 0;
72 | bottom: 0;
73 | }
74 |
75 | .range {
76 | display: flex;
77 | flex-grow: 1;
78 |
79 | .dateWrapper {
80 | overflow: visible;
81 | }
82 |
83 | .wrapper {
84 | &:first-child {
85 | &:before, &:after {
86 | content: '';
87 | position: absolute;
88 | top: 0;
89 | left: 50%;
90 | width: 0;
91 | height: 0;
92 | margin-top: -50px;
93 | margin-left: -50px;
94 |
95 | border-top: 100px solid transparent;
96 | border-bottom: 100px solid transparent;
97 |
98 | border-left: 60px solid;
99 | }
100 |
101 | &:before {
102 | color: rgba(255,255,255, 0.15);
103 | transform: translateX(1px);
104 | }
105 | }
106 | &:last-child {
107 | margin-left: 60px;
108 | }
109 |
110 | .date {
111 | white-space: nowrap;
112 | z-index: 1;
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/Header/Slider/Slider.scss:
--------------------------------------------------------------------------------
1 | .root, .slide {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | }
8 |
9 | .root {
10 | overflow: hidden;
11 | }
12 |
13 | .slide {
14 | padding: 20px 65px;
15 |
16 | &:first-child {
17 | padding-left: 20px;
18 | }
19 | }
20 |
21 | .wrapper {
22 | height: 100%;
23 | transition: transform 0.3s ease;
24 | }
25 |
26 | .arrow {
27 | position: absolute;
28 | top: 0;
29 | z-index: 1;
30 |
31 | display: flex;
32 | align-items: center;
33 | justify-content: center;
34 | width: 40px;
35 | height: 100%;
36 |
37 | opacity: 0.7;
38 | cursor: pointer;
39 | border-left: 1px solid rgba(255,255,255,0.1);
40 |
41 | svg {
42 | width: 15px;
43 | }
44 |
45 | &:hover {
46 | opacity: 1;
47 | }
48 | }
49 |
50 | .arrowRight {
51 | right: 0;
52 | }
53 | .arrowLeft {
54 | left: 0;
55 | transform: scaleX(-1);
56 | }
57 |
--------------------------------------------------------------------------------
/src/Header/Slider/index.js:
--------------------------------------------------------------------------------
1 | import React, {Children, PureComponent} from 'react';
2 | import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
3 | import classNames from 'classnames';
4 | import styles from './Slider.scss';
5 | import transition from './transition.scss';
6 |
7 | const DIRECTIONS = {
8 | LEFT: 0,
9 | RIGHT: 1,
10 | };
11 |
12 | const Arrow = ({direction, onClick}) => (
13 | onClick(direction)}
19 | >
20 |
25 |
26 |
27 |
28 | );
29 |
30 | export default class Slider extends PureComponent {
31 | handleClick = (direction) => {
32 | let {children, index, onChange} = this.props;
33 |
34 | switch (direction) {
35 | case DIRECTIONS.LEFT:
36 | index = Math.max(0, index - 1);
37 | break;
38 | case DIRECTIONS.RIGHT:
39 | index = Math.min(index + 1, children.length);
40 | break;
41 | default:
42 | return;
43 | }
44 |
45 | onChange(index);
46 | }
47 | render() {
48 | const {children, index} = this.props;
49 |
50 | return (
51 |
52 | {index !== 0 &&
53 |
54 | }
55 |
65 | {Children.map(children, (child, i) => (
66 |
71 | {child}
72 |
73 | ))}
74 |
75 | {index !== children.length - 1 &&
76 |
77 | }
78 |
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Header/Slider/transition.scss:
--------------------------------------------------------------------------------
1 | .enter {
2 | opacity: 0;
3 | }
4 | .enterActive {
5 | opacity: 1;
6 | transition: opacity 0.3s ease;
7 | }
8 | .leave {
9 | opacity: 1;
10 | }
11 | .leaveActive {
12 | opacity: 0;
13 | transition: opacity 0.3s ease;
14 | }
15 |
--------------------------------------------------------------------------------
/src/Header/defaultSelectionRenderer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
3 | import classNames from 'classnames';
4 | import parse from 'date-fns/parse';
5 | import format from 'date-fns/format';
6 | import styles from './Header.scss';
7 | import animation from './Animation.scss';
8 |
9 | export default function defaultSelectionRenderer(value, {
10 | display,
11 | key,
12 | locale: {locale},
13 | dateFormat,
14 | onYearClick,
15 | scrollToDate,
16 | setDisplay,
17 | shouldAnimate,
18 | }) {
19 | const date = parse(value);
20 | const values = date && [
21 | {
22 | active: display === 'years',
23 | handleClick: e => {
24 | onYearClick(date, e, key);
25 | setDisplay('years');
26 | },
27 | item: 'year',
28 | title: display === 'days' ? `Change year` : null,
29 | value: date.getFullYear(),
30 | },
31 | {
32 | active: display === 'days',
33 | handleClick: e => {
34 | if (display !== 'days') {
35 | setDisplay('days');
36 | } else if (date) {
37 | scrollToDate(date, -40, true);
38 | }
39 | },
40 | item: 'day',
41 | title: display === 'days'
42 | ? `Scroll to ${format(date, dateFormat, {locale})}`
43 | : null,
44 | value: format(date, dateFormat, {locale}),
45 | },
46 | ];
47 |
48 | return (
49 |
54 | {values.map(({handleClick, item, key, value, active, title}) => {
55 | return (
56 |
63 |
70 |
76 | {value}
77 |
78 |
79 |
80 | );
81 | })}
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/src/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import PropTypes from 'prop-types';
3 | import {emptyFn} from '../utils';
4 | import defaultSelectionRenderer from './defaultSelectionRenderer';
5 | import classNames from 'classnames';
6 | import styles from './Header.scss';
7 |
8 | export default class Header extends PureComponent {
9 | static defaultProps = {
10 | onYearClick: emptyFn,
11 | renderSelection: defaultSelectionRenderer,
12 | };
13 | static propTypes = {
14 | dateFormat: PropTypes.string,
15 | display: PropTypes.string,
16 | layout: PropTypes.string,
17 | locale: PropTypes.object,
18 | onYearClick: PropTypes.func,
19 | selected: PropTypes.any,
20 | shouldAnimate: PropTypes.bool,
21 | theme: PropTypes.object,
22 | };
23 |
24 | render() {
25 | let {
26 | layout,
27 | locale: {blank},
28 | selected,
29 | renderSelection,
30 | theme,
31 | } = this.props;
32 |
33 | return (
34 |
43 | {
44 | selected && renderSelection(selected, this.props) ||
45 |
{blank}
46 | }
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Header/withMultipleDates.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {withImmutableProps} from '../utils';
3 | import defaultSelectionRenderer from './defaultSelectionRenderer';
4 | import Slider from './Slider';
5 | import parse from 'date-fns/parse';
6 | import format from 'date-fns/format';
7 |
8 | export default withImmutableProps(({renderSelection, setDisplayDate}) => ({
9 | renderSelection: (values, {scrollToDate, displayDate, ...props}) => {
10 | if (!values.length) {
11 | return null;
12 | }
13 |
14 | const dates = values.sort();
15 | const index = values.indexOf(format(parse(displayDate), 'YYYY-MM-DD'));
16 |
17 | return (
18 |
21 | setDisplayDate(dates[index], () =>
22 | setTimeout(() => scrollToDate(dates[index], 0, true), 50)
23 | )
24 | }
25 | >
26 | {dates.map(value =>
27 | defaultSelectionRenderer(value, {
28 | ...props,
29 | key: index,
30 | scrollToDate,
31 | shouldAnimate: false,
32 | }))}
33 |
34 | );
35 | },
36 | }));
37 |
--------------------------------------------------------------------------------
/src/Header/withRange.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {withImmutableProps} from '../utils';
3 | import defaultSelectionRenderer from './defaultSelectionRenderer';
4 | import styles from './Header.scss';
5 |
6 | export default withImmutableProps(({renderSelection}) => ({
7 | renderSelection: (values, props) => {
8 | if (!values || !values.start && !values.end) {
9 | return null;
10 | }
11 | if (values.start === values.end) {
12 | return defaultSelectionRenderer(values.start, props);
13 | }
14 |
15 | const dateFormat = props.locale && props.locale.headerFormat || 'MMM Do';
16 |
17 | return (
18 |
19 | {defaultSelectionRenderer(values.start, {...props, dateFormat, key: 'start', shouldAnimate: false})}
20 | {defaultSelectionRenderer(values.end, {...props, dateFormat, key: 'end', shouldAnimate: false})}
21 |
22 | );
23 | },
24 | }));
25 |
--------------------------------------------------------------------------------
/src/Month/Month.scss:
--------------------------------------------------------------------------------
1 | @import "../variables";
2 |
3 | .root {
4 | // position: absolute;
5 | // width: 100%;
6 | }
7 | .rows {
8 | position: relative;
9 | background: linear-gradient(to bottom, rgba(255,255,255,0) 50%,rgba(0,0,0,0.05) 100%);
10 | }
11 | .row {
12 | padding: 0;
13 | margin: 0;
14 |
15 | &:first-child {
16 | text-align: right;
17 |
18 | li {
19 | background-color: #FFF;
20 | box-shadow: 0 -1px 0 $borderColor;
21 | }
22 | }
23 | &:nth-child(2) {
24 | box-shadow: 0 -1px 0 $borderColor;
25 | }
26 | &.partial {
27 | &:first-child {
28 | li {
29 | &:first-child {
30 | box-shadow: 0px -1px 0 #E9E9E9, inset 1px 0 0 #E9E9E9;
31 | }
32 | }
33 | }
34 | &:last-of-type {
35 | li {
36 | position: relative;
37 | z-index: 1;
38 | }
39 | }
40 | }
41 | }
42 |
43 | .label {
44 | position: absolute;
45 | top: 0;
46 | bottom: 0;
47 | left: 0;
48 | right: 0;
49 |
50 | margin: 0;
51 |
52 | font-size: 30px;
53 | font-weight: 500;
54 | z-index: 3;
55 | pointer-events: none;
56 |
57 | background-color: rgba(255,255,255,0.6);
58 |
59 | opacity: 0;
60 | transition: opacity 0.3s ease;
61 |
62 | > span {
63 | position: absolute;
64 | left: 0;
65 | right: 0;
66 | top: 0;
67 | bottom: $rowHeight;
68 |
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 |
73 | text-transform: capitalize;
74 | }
75 |
76 | &.partialFirstRow {
77 | top: $rowHeight;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Month/index.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import classNames from 'classnames';
3 | import {getDateString} from '../utils';
4 | import format from 'date-fns/format';
5 | import getDay from 'date-fns/get_day';
6 | import isSameYear from 'date-fns/is_same_year';
7 | import styles from './Month.scss';
8 |
9 | export default class Month extends PureComponent {
10 | renderRows() {
11 | const {
12 | DayComponent,
13 | disabledDates,
14 | disabledDays,
15 | monthDate,
16 | locale,
17 | maxDate,
18 | minDate,
19 | rowHeight,
20 | rows,
21 | selected,
22 | today,
23 | theme,
24 | passThrough,
25 | } = this.props;
26 | const currentYear = today.getFullYear();
27 | const year = monthDate.getFullYear();
28 | const month = monthDate.getMonth();
29 | const monthShort = format(monthDate, 'MMM', {locale: locale.locale});
30 | const monthRows = [];
31 | let day = 0;
32 | let isDisabled = false;
33 | let isToday = false;
34 | let date, days, dow, row;
35 |
36 | // Used for faster comparisons
37 | const _today = format(today, 'YYYY-MM-DD');
38 | const _minDate = format(minDate, 'YYYY-MM-DD');
39 | const _maxDate = format(maxDate, 'YYYY-MM-DD');
40 |
41 | // Oh the things we do in the name of performance...
42 | for (let i = 0, len = rows.length; i < len; i++) {
43 | row = rows[i];
44 | days = [];
45 | dow = getDay(new Date(year, month, row[0]));
46 |
47 | for (let k = 0, len = row.length; k < len; k++) {
48 | day = row[k];
49 |
50 | date = getDateString(year, month, day);
51 | isToday = (date === _today);
52 |
53 | isDisabled = (
54 | minDate && date < _minDate ||
55 | maxDate && date > _maxDate ||
56 | disabledDays && disabledDays.length && disabledDays.indexOf(dow) !== -1 ||
57 | disabledDates && disabledDates.length && disabledDates.indexOf(date) !== -1
58 | );
59 |
60 | days[k] = (
61 |
76 | );
77 |
78 | dow += 1;
79 | }
80 | monthRows[i] = (
81 |
90 | );
91 |
92 | }
93 |
94 | return monthRows;
95 | }
96 |
97 | render() {
98 | const {locale: {locale}, monthDate, today, rows, rowHeight, showOverlay, style, theme} = this.props;
99 | const dateFormat = isSameYear(monthDate, today) ? 'MMMM' : 'MMMM YYYY';
100 |
101 | return (
102 |
103 |
104 | {this.renderRows()}
105 | {showOverlay &&
106 |
112 | {format(monthDate, dateFormat, {locale})}
113 |
114 | }
115 |
116 |
117 | );
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/MonthList/MonthList.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 100% !important;
3 | background-color: #FFF;
4 | -webkit-overflow-scrolling: touch;
5 |
6 | &.scrolling {
7 | > div {
8 | pointer-events: none;
9 | }
10 |
11 | label {
12 | opacity: 1;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/MonthList/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import VirtualList from 'react-tiny-virtual-list';
4 | import classNames from 'classnames';
5 | import {
6 | emptyFn,
7 | getMonth,
8 | getWeek,
9 | getWeeksInMonth,
10 | animate,
11 | } from '../utils';
12 | import parse from 'date-fns/parse';
13 | import startOfMonth from 'date-fns/start_of_month';
14 | import Month from '../Month';
15 | import styles from './MonthList.scss';
16 |
17 | const AVERAGE_ROWS_PER_MONTH = 5;
18 |
19 | export default class MonthList extends Component {
20 | static propTypes = {
21 | disabledDates: PropTypes.arrayOf(PropTypes.string),
22 | disabledDays: PropTypes.arrayOf(PropTypes.number),
23 | height: PropTypes.number,
24 | isScrolling: PropTypes.bool,
25 | locale: PropTypes.object,
26 | maxDate: PropTypes.instanceOf(Date),
27 | min: PropTypes.instanceOf(Date),
28 | minDate: PropTypes.instanceOf(Date),
29 | months: PropTypes.arrayOf(PropTypes.object),
30 | onDaySelect: PropTypes.func,
31 | onScroll: PropTypes.func,
32 | overscanMonthCount: PropTypes.number,
33 | rowHeight: PropTypes.number,
34 | selectedDate: PropTypes.instanceOf(Date),
35 | showOverlay: PropTypes.bool,
36 | theme: PropTypes.object,
37 | today: PropTypes.instanceOf(Date),
38 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
39 | };
40 | state = {
41 | scrollTop: this.getDateOffset(this.props.scrollDate),
42 | };
43 | cache = {};
44 | memoize = function(param) {
45 | if (!this.cache[param]) {
46 | const {locale: {weekStartsOn}} = this.props;
47 | const [year, month] = param.split(':');
48 | const result = getMonth(year, month, weekStartsOn);
49 | this.cache[param] = result;
50 | }
51 | return this.cache[param];
52 | };
53 | monthHeights = [];
54 |
55 | _getRef = (instance) => { this.VirtualList = instance; }
56 |
57 | getMonthHeight = (index) => {
58 | if (!this.monthHeights[index]) {
59 | let {locale: {weekStartsOn}, months, rowHeight} = this.props;
60 | let {month, year} = months[index];
61 | let weeks = getWeeksInMonth(month, year, weekStartsOn, index === months.length - 1);
62 | let height = weeks * rowHeight;
63 | this.monthHeights[index] = height;
64 | }
65 |
66 | return this.monthHeights[index];
67 | };
68 |
69 | componentDidMount() {
70 | this.scrollEl = this.VirtualList.rootNode;
71 | }
72 |
73 | componentWillReceiveProps({scrollDate}) {
74 | if (scrollDate !== this.props.scrollDate) {
75 | this.setState({
76 | scrollTop: this.getDateOffset(scrollDate),
77 | });
78 | }
79 | }
80 |
81 | getDateOffset(date) {
82 | const {min, rowHeight, locale: {weekStartsOn}, height} = this.props;
83 | const weeks = getWeek(startOfMonth(min), parse(date), weekStartsOn);
84 |
85 | return weeks * rowHeight - (height - rowHeight/2) / 2;
86 | }
87 |
88 | scrollToDate = (date, offset = 0, ...rest) => {
89 | let offsetTop = this.getDateOffset(date);
90 | this.scrollTo(offsetTop + offset, ...rest);
91 | };
92 |
93 | scrollTo = (scrollTop = 0, shouldAnimate = false, onScrollEnd = emptyFn) => {
94 | const onComplete = () => setTimeout(() => {
95 | this.scrollEl.style.overflowY = 'auto';
96 | onScrollEnd();
97 | });
98 |
99 | // Interrupt iOS Momentum scroll
100 | this.scrollEl.style.overflowY = 'hidden';
101 |
102 | if (shouldAnimate) {
103 | /* eslint-disable sort-keys */
104 | animate({
105 | fromValue: this.scrollEl.scrollTop,
106 | toValue: scrollTop,
107 | onUpdate: (scrollTop, callback) =>
108 | this.setState({scrollTop}, callback),
109 | onComplete,
110 | });
111 | } else {
112 | window.requestAnimationFrame(() => {
113 | this.scrollEl.scrollTop = scrollTop;
114 | onComplete();
115 | });
116 | }
117 | };
118 |
119 | renderMonth = ({index, style}) => {
120 | let {
121 | DayComponent,
122 | disabledDates,
123 | disabledDays,
124 | locale,
125 | maxDate,
126 | minDate,
127 | months,
128 | passThrough,
129 | rowHeight,
130 | selected,
131 | showOverlay,
132 | theme,
133 | today,
134 | } = this.props;
135 |
136 | let {month, year} = months[index];
137 | let key = `${year}:${month}`;
138 | let {date, rows} = this.memoize(key);
139 |
140 | return (
141 |
161 | );
162 | };
163 |
164 | render() {
165 | let {
166 | height,
167 | isScrolling,
168 | onScroll,
169 | overscanMonthCount,
170 | months,
171 | rowHeight,
172 | width,
173 | } = this.props;
174 | const {scrollTop} = this.state;
175 |
176 | return (
177 |
191 | );
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/Today/Today.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | height: 32px;
10 | padding: 6px;
11 | box-sizing: border-box;
12 | transform: translate3d(0, -100%, 0);
13 |
14 | font-weight: 500;
15 | line-height: 0;
16 | z-index: 10;
17 | cursor: pointer;
18 |
19 | transition: transform 0.3s ease;
20 | transition-delay: 0.3s;
21 |
22 | &.show {
23 | transform: translate3d(0, 0, 0);
24 | transition-delay: 0s;
25 |
26 | .chevron {
27 | transition: transform 0.3s ease;
28 | }
29 | }
30 |
31 | .chevron {
32 | position: absolute;
33 | top: 50%;
34 | margin-top: -6px;
35 | margin-left: 5px;
36 | transform: rotate(270deg);
37 | transition: transform 0.3s ease;
38 | }
39 |
40 | &.chevronUp .chevron {
41 | transform: rotate(180deg);
42 | }
43 |
44 | &.chevronDown .chevron {
45 | transform: rotate(360deg);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Today/index.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 | import styles from './Today.scss';
5 |
6 | export const DIRECTION_UP = 1;
7 | export const DIRECTION_DOWN = -1;
8 | const CHEVRON = 'M256,298.3L256,298.3L256,298.3l174.2-167.2c4.3-4.2,11.4-4.1,15.8,0.2l30.6,29.9c4.4,4.3,4.5,11.3,0.2,15.5L264.1,380.9 c-2.2,2.2-5.2,3.2-8.1,3c-3,0.1-5.9-0.9-8.1-3L35.2,176.7c-4.3-4.2-4.2-11.2,0.2-15.5L66,131.3c4.4-4.3,11.5-4.4,15.8-0.2L256,298.3 z';
9 |
10 | export default class Today extends PureComponent {
11 | static propTypes = {
12 | scrollToDate: PropTypes.func,
13 | show: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
14 | theme: PropTypes.object,
15 | todayLabel: PropTypes.string,
16 | };
17 |
18 | scrollToToday = () => {
19 | let {scrollToDate} = this.props;
20 |
21 | scrollToDate(new Date(), -40, true);
22 | };
23 |
24 | render() {
25 | let {todayLabel, show, theme} = this.props;
26 | return (
27 |
40 | {todayLabel}
41 |
49 |
53 |
54 |
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Weekdays/Weekdays.scss:
--------------------------------------------------------------------------------
1 | @import "../variables";
2 |
3 | .root {
4 | position: relative;
5 | z-index: 5;
6 |
7 | display: flex;
8 | padding: 0;
9 | margin: 0;
10 | list-style: none;
11 | box-shadow: inset 0 -1px rgba(0,0,0,0.04);
12 | }
13 |
14 | .day {
15 | padding: 15px 0;
16 | flex-basis: 1 / 7 * 100%;
17 | flex-grow: 1;
18 | font-weight: 500;
19 | text-align: center;
20 | }
21 |
--------------------------------------------------------------------------------
/src/Weekdays/index.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import PropTypes from 'prop-types';
3 | import {scrollbarSize} from '../utils';
4 | import styles from './Weekdays.scss';
5 |
6 | export default class Weekdays extends PureComponent {
7 | static propTypes = {
8 | locale: PropTypes.object,
9 | theme: PropTypes.object,
10 | };
11 |
12 | render() {
13 | const {weekdays, weekStartsOn, theme} = this.props;
14 | const orderedWeekdays = [...weekdays.slice(weekStartsOn, 7), ...weekdays.slice(0, weekStartsOn)];
15 |
16 | return (
17 |
26 | {orderedWeekdays.map((val, index) => (
27 | {val}
28 | ))}
29 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Years/Years.scss:
--------------------------------------------------------------------------------
1 | @import "../variables";
2 |
3 | .root {
4 | position: absolute;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 | z-index: 10;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | background-color: #F9F9F9;
13 |
14 | &:before, &:after {
15 | content: '';
16 | position: absolute;
17 | left: 0;
18 | right: 0;
19 | height: 50px;
20 | pointer-events: none;
21 | z-index: 1;
22 | }
23 | &:before {
24 | top: 0;
25 | background: linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, rgba(255, 255, 255, 0) 100%);
26 | }
27 | &:after {
28 | bottom: 0;
29 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255,255,255,0.8) 100%);
30 | }
31 | }
32 |
33 | .list {
34 | box-sizing: border-box;
35 |
36 | &.center {
37 | display: flex;
38 | align-items: center;
39 | justify-content: center;
40 | }
41 | }
42 |
43 | .year {
44 | $cellSize: 44px;
45 |
46 | display: flex;
47 | padding: 0 20px;
48 | height: 100%;
49 | align-items: center;
50 | justify-content: center;
51 | font-size: 18px;
52 | font-weight: 500;
53 | text-align: center;
54 | cursor: pointer;
55 | -webkit-user-select: none;
56 | box-sizing: border-box;
57 |
58 | &.withMonths {
59 | border-bottom: 1px solid $borderColor;
60 |
61 | label {
62 | height: $cellSize * 2;
63 | padding-top: 12px;
64 | box-sizing: border-box;
65 | }
66 | }
67 |
68 | label {
69 | flex-basis: 42%;
70 |
71 | span {
72 | flex-shrink: 0;
73 | color: #333;
74 | }
75 | }
76 |
77 | ol {
78 | display: flex;
79 | flex-wrap: wrap;
80 | margin: 0;
81 | padding: 0;
82 |
83 | font-size: 14px;
84 |
85 | li {
86 | display: flex;
87 | width: $cellSize;
88 | height: $cellSize;
89 | flex-shrink: 0;
90 | align-items: center;
91 | justify-content: center;
92 | list-style: none;
93 | border-radius: 50%;
94 | box-sizing: border-box;
95 |
96 | color: #444;
97 | font-weight: 400;
98 | text-transform: capitalize;
99 |
100 | &.currentMonth {
101 | border: 1px solid;
102 | }
103 | &.selected {
104 | position: relative;
105 | z-index: 1;
106 | background-color: blue;
107 | color: #FFF !important;
108 | border: 0;
109 | }
110 | &.disabled {
111 | cursor: not-allowed;
112 | color: $textColorDisabled;
113 |
114 | &:hover {
115 | background-color: inherit;
116 | }
117 | }
118 |
119 | &:hover {
120 | background-color: $cellHoverBg;
121 | }
122 | }
123 | }
124 |
125 | &:hover, &.active {
126 | label > span {
127 | color: inherit;
128 | }
129 | }
130 | &:hover, &.active {
131 | position: relative;
132 | z-index: 2;
133 | }
134 | &.active {
135 | font-size: 32px;
136 | }
137 | &.currentYear {
138 | position: relative;
139 |
140 | label > span {
141 | min-width: 50px;
142 | padding-bottom: 5px;
143 | border-bottom: 3px solid;
144 | }
145 |
146 | &.active {
147 | label > span {
148 | min-width: 85px;
149 | }
150 | }
151 | }
152 |
153 | // Spacing
154 | $spacing: 40px;
155 | &.first {
156 | padding-top: $spacing;
157 | }
158 | &.last {
159 | padding-bottom: $spacing;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Years/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import VirtualList from 'react-tiny-virtual-list';
4 | import classNames from 'classnames';
5 | import {emptyFn, getMonthsForYear} from '../utils';
6 | import format from 'date-fns/format';
7 | import isAfter from 'date-fns/is_after';
8 | import isBefore from 'date-fns/is_before';
9 | import isSameMonth from 'date-fns/is_same_month';
10 | import styles from './Years.scss';
11 |
12 | const SPACING = 40;
13 |
14 | export default class Years extends Component {
15 | static propTypes = {
16 | height: PropTypes.number,
17 | hideOnSelect: PropTypes.bool,
18 | locale: PropTypes.object,
19 | max: PropTypes.object,
20 | maxDate: PropTypes.object,
21 | min: PropTypes.object,
22 | minDate: PropTypes.object,
23 | onSelect: PropTypes.func,
24 | scrollToDate: PropTypes.func,
25 | selectedYear: PropTypes.number,
26 | setDisplay: PropTypes.func,
27 | theme: PropTypes.object,
28 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
29 | years: PropTypes.array,
30 | };
31 | static defaultProps = {
32 | onSelect: emptyFn,
33 | showMonths: true,
34 | };
35 |
36 | handleClick(date, e) {
37 | let {
38 | hideOnSelect,
39 | onSelect,
40 | setDisplay,
41 | scrollToDate,
42 | } = this.props;
43 |
44 | onSelect(date, e, (date) => scrollToDate(date));
45 |
46 | if (hideOnSelect) {
47 | window.requestAnimationFrame(() => setDisplay('days'));
48 | }
49 | }
50 |
51 | renderMonths(year) {
52 | const {locale: {locale}, selected, theme, today, min, max, minDate, maxDate} = this.props;
53 | const months = getMonthsForYear(year, selected.getDate());
54 |
55 | return (
56 |
57 | {months.map((date, index) => {
58 | const isSelected = isSameMonth(date, selected);
59 | const isCurrentMonth = isSameMonth(date, today);
60 | const isDisabled = (
61 | isBefore(date, min) ||
62 | isBefore(date, minDate) ||
63 | isAfter(date, max) ||
64 | isAfter(date, maxDate)
65 | );
66 | const style = Object.assign({}, isSelected && {
67 | backgroundColor: (
68 | typeof theme.selectionColor === 'function'
69 | ? theme.selectionColor(date)
70 | : theme.selectionColor
71 | ),
72 | }, isCurrentMonth && {
73 | borderColor: theme.todayColor,
74 | });
75 |
76 | return (
77 | {
80 | e.stopPropagation();
81 |
82 | if (!isDisabled) {
83 | this.handleClick(date, e);
84 | }
85 | }}
86 | className={classNames(styles.month, {
87 | [styles.selected]: isSelected,
88 | [styles.currentMonth]: isCurrentMonth,
89 | [styles.disabled]: isDisabled,
90 | })}
91 | style={style}
92 | title={`Set date to ${format(date, 'MMMM Do, YYYY')}`}
93 | >
94 | {format(date, 'MMM', {locale})}
95 |
96 | );
97 | })}
98 |
99 | );
100 | }
101 |
102 | render() {
103 | const {height, selected, showMonths, theme, today, width} = this.props;
104 | const currentYear = today.getFullYear();
105 | const years = this.props.years.slice(0, this.props.years.length);
106 | const selectedYearIndex = years.indexOf(selected.getFullYear());
107 | const rowHeight = showMonths ? 110 : 50;
108 | const heights = years.map((val, index) => index === 0 || index === years.length - 1
109 | ? rowHeight + SPACING
110 | : rowHeight
111 | );
112 | const containerHeight = years.length * rowHeight < height + 50
113 | ? years.length * rowHeight
114 | : height + 50;
115 |
116 | return (
117 |
121 |
heights[index]}
129 | scrollToIndex={selectedYearIndex !== -1 ? selectedYearIndex : null}
130 | scrollToAlignment='center'
131 | renderItem={({index, style}) => {
132 | const year = years[index];
133 | const isActive = index === selectedYearIndex;
134 |
135 | return (
136 | this.handleClick(new Date(selected).setYear(year))}
146 | title={`Set year to ${year}`}
147 | data-year={year}
148 | style={Object.assign({}, style, {
149 | color: (
150 | typeof theme.selectionColor === 'function'
151 | ? theme.selectionColor(new Date(year, 0, 1))
152 | : theme.selectionColor
153 | ),
154 | })}
155 | >
156 |
157 |
164 | {year}
165 |
166 |
167 | {showMonths && this.renderMonths(year)}
168 |
169 | );
170 | }}
171 | />
172 |
173 | );
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/_variables.scss:
--------------------------------------------------------------------------------
1 | // @Variables
2 | $textColor: #3d3d3d;
3 | $textColorDisabled: #AAA;
4 | $rowHeight: 56px;
5 | $borderRadius: 3px;
6 | $borderColor: #E9E9E9;
7 | $cellHoverBg: #EFEFEF;
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import Calendar from './Calendar';
3 | import {withDateSelection} from './Calendar/withDateSelection';
4 |
5 | export {default as Calendar} from './Calendar';
6 | export {withDateSelection} from './Calendar/withDateSelection';
7 | export {withKeyboardSupport} from './Calendar/withKeyboardSupport';
8 | export {withMultipleDates, defaultMultipleDateInterpolation} from './Calendar/withMultipleDates';
9 | export {withRange, EVENT_TYPE} from './Calendar/withRange';
10 |
11 | /*
12 | * By default, Calendar is a controlled component.
13 | * Export a sensible default for minimal setup
14 | */
15 | export default class DefaultCalendar extends Component {
16 | static defaultProps = {
17 | Component: withDateSelection(Calendar),
18 | interpolateSelection: (selected) => selected,
19 | };
20 | state = {
21 | selected: typeof this.props.selected !== 'undefined'
22 | ? this.props.selected
23 | : new Date(),
24 | };
25 | componentWillReceiveProps({selected}) {
26 | if (selected !== this.props.selected) {
27 | this.setState({selected});
28 | }
29 | }
30 | handleSelect = (selected) => {
31 | const {onSelect, interpolateSelection} = this.props;
32 |
33 | if (typeof onSelect === 'function') { onSelect(selected); }
34 |
35 | this.setState({selected: interpolateSelection(selected, this.state.selected)});
36 | }
37 | render() {
38 | // eslint-disable-next-line no-unused-vars
39 | const {Component, interpolateSelection, ...props} = this.props;
40 |
41 | return (
42 |
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/animate.js:
--------------------------------------------------------------------------------
1 | function easing(time) {
2 | return 1 - (--time) * time * time * time;
3 | };
4 |
5 | /**
6 | * Given a start/end point of a scroll and time elapsed, calculate the scroll position we should be at
7 | * @param {Number} start - the initial value
8 | * @param {Number} stop - the final desired value
9 | * @param {Number} elapsed - the amount of time elapsed since we started animating
10 | * @param {Number} - duration - the duration of the animation
11 | * @return {Number} - The value we should use on the next tick
12 | */
13 | function getValue(start, end, elapsed, duration) {
14 | if (elapsed > duration) return end;
15 | return start + (end - start) * easing(elapsed / duration);
16 | };
17 |
18 | /**
19 | * Smoothly animate between two values
20 | * @param {Number} fromValue - the initial value
21 | * @param {Function} onUpdate - A function that is called on each tick
22 | * @param {Function} onComplete - A callback that is fired once the scroll animation ends
23 | * @param {Number} duration - the desired duration of the scroll animation
24 | */
25 | export default function animate({
26 | fromValue,
27 | toValue,
28 | onUpdate,
29 | onComplete,
30 | duration = 600,
31 | }) {
32 | const startTime = performance.now();
33 |
34 | const tick = () => {
35 | const elapsed = performance.now() - startTime;
36 |
37 | window.requestAnimationFrame(() => onUpdate(
38 | getValue(fromValue, toValue, elapsed, duration),
39 | // Callback
40 | elapsed <= duration
41 | ? tick
42 | : onComplete
43 | ));
44 | };
45 |
46 | tick();
47 | };
48 |
--------------------------------------------------------------------------------
/src/utils/defaultDisplayOptions.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | hideYearsOnSelect: true,
3 | layout: 'portrait',
4 | overscanMonthCount: 2,
5 | shouldHeaderAnimate: true,
6 | showHeader: true,
7 | showMonthsForYears: true,
8 | showOverlay: true,
9 | showTodayHelper: true,
10 | showWeekdays: true,
11 | todayHelperRowOffset: 4,
12 | };
13 |
--------------------------------------------------------------------------------
/src/utils/defaultLocale.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | blank: 'Select a date...',
3 | headerFormat: 'ddd, MMM Do',
4 | todayLabel: {
5 | long: 'Today',
6 | },
7 | weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
8 | weekStartsOn: 0,
9 | };
10 |
--------------------------------------------------------------------------------
/src/utils/defaultTheme.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | accentColor: '#448AFF',
3 | floatingNav: {
4 | background: 'rgba(56, 87, 138, 0.94)',
5 | chevron: '#FFA726',
6 | color: '#FFF',
7 | },
8 | headerColor: '#448AFF',
9 | selectionColor: '#559FFF',
10 | textColor: {
11 | active: '#FFF',
12 | default: '#333',
13 | },
14 | todayColor: '#FFA726',
15 | weekdayColor: '#559FFF',
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import getScrollbarSize from 'dom-helpers/util/scrollbarSize';
2 | import getDaysInMonth from 'date-fns/get_days_in_month';
3 | import getDay from 'date-fns/get_day';
4 | import isAfter from 'date-fns/is_after';
5 | import isBefore from 'date-fns/is_before';
6 | import isSameDay from 'date-fns/is_same_day';
7 | import endOfDay from 'date-fns/end_of_day';
8 | import startOfDay from 'date-fns/start_of_day';
9 | import {withPropsOnChange} from 'recompose';
10 |
11 | export const keyCodes = {
12 | command: 91,
13 | control: 17,
14 | down: 40,
15 | enter: 13,
16 | escape: 27,
17 | left: 37,
18 | right: 39,
19 | shift: 16,
20 | up: 38,
21 | };
22 |
23 | /**
24 | * Given a year and a month, returns the rows for that month to be iterated over
25 | * @param {Number} year - the year number
26 | * @param {Number} month - the index of the month
27 | * @param {Number} weekStartsOn - the index of the first day of the week (from 0 to 6)
28 | * @return {Object} - Returns the first day of the month and the rows of that month
29 | */
30 | export function getMonth(year, month, weekStartsOn) {
31 | const rows = [];
32 | const monthDate = new Date(year, month, 1);
33 | const daysInMonth = getDaysInMonth(monthDate);
34 | const weekEndsOn = getEndOfWeekIndex(weekStartsOn);
35 |
36 | let dow = getDay(new Date(year, month, 1));
37 | let week = 0;
38 |
39 | for (let day = 1; day <= daysInMonth; day++) {
40 | if (!rows[week]) {
41 | rows[week] = [];
42 | }
43 |
44 | rows[week].push(day);
45 |
46 | if (dow === weekEndsOn) {
47 | week++;
48 | }
49 |
50 | dow = dow < 6 ? dow + 1 : 0;
51 | }
52 |
53 | return {
54 | date: monthDate,
55 | rows,
56 | };
57 | }
58 |
59 | export function getWeek(yearStart, date, weekStartsOn) {
60 | const yearStartDate = (typeof yearStart === 'number')
61 | ? new Date(yearStart, 0, 1) // 1st Jan of the Year
62 | : yearStart;
63 |
64 | return Math.ceil(
65 | (Math.round((date - yearStartDate) / (60 * 60 * 24 * 1000)) + yearStartDate.getDay() + 1 - weekStartsOn) / 7
66 | );
67 | }
68 |
69 | /**
70 | * Get the number of weeks in a given month to be able to calculate the height of that month
71 | * @param {Number} year - the year number
72 | * @param {Number} month - the index of the month
73 | * @param {Number} weekStartsOn - the index of the first day of the week (from 0 to 6)
74 | * @return {Number} - Returns the number of weeks for the given month
75 | */
76 | export function getWeeksInMonth(
77 | month,
78 | year = new Date().getFullYear(),
79 | weekStartsOn,
80 | isLastDisplayedMonth
81 | ) {
82 | const weekEndsOn = getEndOfWeekIndex(weekStartsOn);
83 |
84 | const firstOfMonth = new Date(year, month, 1);
85 | const firstWeekNumber = getWeek(year, firstOfMonth, weekStartsOn);
86 |
87 | const lastOfMonth = new Date(year, month + 1, 0); // Last date of the Month
88 | const lastWeekNumber = getWeek(year, lastOfMonth, weekStartsOn);
89 |
90 | let rowCount = lastWeekNumber - firstWeekNumber;
91 |
92 | // If the last week contains 7 days, we need to add an extra row
93 | if (lastOfMonth.getDay() === weekEndsOn || isLastDisplayedMonth) {
94 | rowCount++;
95 | }
96 |
97 | return rowCount;
98 | }
99 |
100 | /**
101 | * Helper to find out what day the week ends on given the localized start of the week
102 | * @param {Number} weekStartsOn - the index of the first day of the week (from 0 to 6)
103 | * @return {Number} - Returns the index of the day the week ends on
104 | */
105 | function getEndOfWeekIndex(weekStartsOn) {
106 | const weekEndsOn = weekStartsOn === 0 ? 6 : weekStartsOn - 1;
107 |
108 | return weekEndsOn;
109 | }
110 |
111 | export class ScrollSpeed {
112 | clear = () => {
113 | this.lastPosition = null;
114 | this.delta = 0;
115 | };
116 | getScrollSpeed(scrollOffset) {
117 | if (this.lastPosition != null) {
118 | this.delta = scrollOffset - this.lastPosition;
119 | }
120 | this.lastPosition = scrollOffset;
121 |
122 | clearTimeout(this._timeout);
123 | this._timeout = setTimeout(this.clear, 50);
124 |
125 | return this.delta;
126 | }
127 | }
128 |
129 | export const scrollbarSize = getScrollbarSize();
130 |
131 | export function emptyFn() {
132 | /* no-op */
133 | }
134 |
135 | export function sanitizeDate(date, {
136 | disabledDates = [],
137 | disabledDays = [],
138 | minDate,
139 | maxDate,
140 | }) {
141 | // Selected date should not be disabled or outside the selectable range
142 | if (
143 | !date ||
144 | disabledDates.some(disabledDate => isSameDay(disabledDate, date)) ||
145 | disabledDays && disabledDays.indexOf(getDay(date)) !== -1 ||
146 | minDate && isBefore(date, startOfDay(minDate)) ||
147 | maxDate && isAfter(date, endOfDay(maxDate))
148 | ) {
149 | return null;
150 | }
151 |
152 | return date;
153 | }
154 |
155 | export function getDateString(year, month, date) {
156 | return `${year}-${('0' + (month + 1)).slice(-2)}-${('0' + date).slice(-2)}`;
157 | }
158 |
159 | export function getMonthsForYear(year, day = 1) {
160 | return Array.apply(null, Array(12)).map((val, index) => new Date(year, index, day));
161 | }
162 |
163 | export const withImmutableProps = (props) => withPropsOnChange(() => false, props);
164 |
165 | export function debounce(callback, wait) {
166 | let timeout = null;
167 | let callbackArgs = null;
168 |
169 | const later = () => callback.apply(this, callbackArgs);
170 |
171 | return function() {
172 | callbackArgs = arguments;
173 | clearTimeout(timeout);
174 | timeout = setTimeout(later, wait);
175 | };
176 | }
177 |
178 | export function range(start, stop, step = 1) {
179 | const length = Math.max(Math.ceil((stop - start) / step), 0);
180 | const range = Array(length);
181 |
182 | for (let i = 0; i < length; i++, start += step) {
183 | range[i] = start;
184 | }
185 |
186 | return range;
187 | };
188 |
189 | export {default as animate} from './animate';
190 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | .Cal__Day__root {
2 | display: inline-block;
3 | box-sizing: border-box;
4 | width: 14.28571%;
5 | list-style: none;
6 | font-size: 16px;
7 | text-align: center;
8 | cursor: pointer;
9 | user-select: none; }
10 | .Cal__Day__root.Cal__Day__enabled.Cal__Day__highlighted, .Cal__Day__root.Cal__Day__enabled:active, .Cal__Day__root.Cal__Day__enabled:hover {
11 | position: relative;
12 | z-index: 1; }
13 | .Cal__Day__root.Cal__Day__enabled.Cal__Day__highlighted:before, .Cal__Day__root.Cal__Day__enabled:active:before, .Cal__Day__root.Cal__Day__enabled:hover:before {
14 | content: '';
15 | position: absolute;
16 | top: 50%;
17 | left: 50%;
18 | width: 52px;
19 | height: 52px;
20 | margin-top: -26px;
21 | margin-left: -26px;
22 | border-radius: 50%;
23 | background-color: #EFEFEF;
24 | z-index: -1; }
25 | .Cal__Day__root.Cal__Day__enabled:hover:before {
26 | opacity: 0.5; }
27 | .Cal__Day__root.Cal__Day__enabled.Cal__Day__highlighted:before, .Cal__Day__root.Cal__Day__enabled:active:before {
28 | opacity: 1; }
29 | .Cal__Day__root:first-child {
30 | position: relative; }
31 | .Cal__Day__root.Cal__Day__today {
32 | position: relative;
33 | z-index: 2; }
34 | .Cal__Day__root.Cal__Day__today > span {
35 | color: #3d3d3d; }
36 | .Cal__Day__root.Cal__Day__today.Cal__Day__disabled > span {
37 | color: #AAA; }
38 | .Cal__Day__root.Cal__Day__today:before {
39 | content: '';
40 | position: absolute;
41 | top: 50%;
42 | left: 50%;
43 | width: 52px;
44 | height: 52px;
45 | margin-top: -26px;
46 | margin-left: -26px;
47 | border-radius: 50%;
48 | box-shadow: inset 0 0 0 1px;
49 | z-index: -1; }
50 | .Cal__Day__root.Cal__Day__today.Cal__Day__disabled:before {
51 | box-shadow: inset 0 0 0 1px #BBB; }
52 | .Cal__Day__root.Cal__Day__selected {
53 | position: relative; }
54 | .Cal__Day__root.Cal__Day__selected > .Cal__Day__month, .Cal__Day__root.Cal__Day__selected > .Cal__Day__year {
55 | display: none; }
56 | .Cal__Day__root.Cal__Day__selected:before {
57 | display: none; }
58 | .Cal__Day__root.Cal__Day__selected .Cal__Day__selection {
59 | content: '';
60 | position: absolute;
61 | top: 50%;
62 | left: 50%;
63 | width: 52px;
64 | height: 52px;
65 | margin-top: -26px;
66 | margin-left: -26px;
67 | border-radius: 50%;
68 | line-height: 56px;
69 | z-index: 2; }
70 | .Cal__Day__root.Cal__Day__selected .Cal__Day__selection .Cal__Day__month {
71 | top: 9px; }
72 | .Cal__Day__root.Cal__Day__selected .Cal__Day__selection .Cal__Day__day {
73 | position: relative;
74 | top: 5px;
75 | font-weight: bold;
76 | font-size: 18px; }
77 | .Cal__Day__root.Cal__Day__disabled {
78 | color: #AAA;
79 | cursor: not-allowed; }
80 |
81 | .Cal__Day__month, .Cal__Day__year {
82 | position: absolute;
83 | left: 0;
84 | right: 0;
85 | font-size: 12px;
86 | line-height: 12px;
87 | text-transform: capitalize; }
88 |
89 | .Cal__Day__month {
90 | top: 5px; }
91 |
92 | .Cal__Day__year {
93 | bottom: 5px; }
94 |
95 | /*
96 | * Range selection styles
97 | */
98 | .Cal__Day__range.Cal__Day__selected.Cal__Day__start:after, .Cal__Day__range.Cal__Day__selected.Cal__Day__end:after {
99 | content: '';
100 | position: absolute;
101 | top: 50%;
102 | width: 50%;
103 | height: 52px;
104 | margin-top: -26px;
105 | box-shadow: inset 56px 56px; }
106 |
107 | .Cal__Day__range.Cal__Day__selected.Cal__Day__disabled .Cal__Day__selection.Cal__Day__selection {
108 | background-color: #EEE !important; }
109 | .Cal__Day__range.Cal__Day__selected.Cal__Day__disabled .Cal__Day__selection.Cal__Day__selection .Cal__Day__day, .Cal__Day__range.Cal__Day__selected.Cal__Day__disabled .Cal__Day__selection.Cal__Day__selection .Cal__Day__month {
110 | color: #AAA;
111 | font-weight: 300; }
112 |
113 | .Cal__Day__range.Cal__Day__selected.Cal__Day__start .Cal__Day__selection {
114 | border-top-left-radius: 50%;
115 | border-bottom-left-radius: 50%; }
116 |
117 | .Cal__Day__range.Cal__Day__selected.Cal__Day__start:after {
118 | right: 0; }
119 |
120 | .Cal__Day__range.Cal__Day__selected.Cal__Day__start.Cal__Day__end:after {
121 | display: none; }
122 |
123 | .Cal__Day__range.Cal__Day__selected.Cal__Day__betweenRange .Cal__Day__selection {
124 | left: 0;
125 | right: 0;
126 | width: 100%;
127 | margin-left: 0;
128 | display: flex;
129 | justify-content: center;
130 | align-items: center;
131 | border-radius: 0; }
132 |
133 | .Cal__Day__range.Cal__Day__selected.Cal__Day__betweenRange .Cal__Day__day {
134 | top: 0;
135 | font-size: 16px; }
136 |
137 | .Cal__Day__range.Cal__Day__selected.Cal__Day__betweenRange .Cal__Day__month {
138 | display: none; }
139 |
140 | .Cal__Day__range.Cal__Day__selected.Cal__Day__end:after {
141 | left: 0; }
142 |
143 | .Cal__Day__range.Cal__Day__selected.Cal__Day__end .Cal__Day__selection {
144 | border-top-right-radius: 50%;
145 | border-bottom-right-radius: 50%;
146 | color: inherit !important;
147 | background-color: #FFF !important;
148 | border: 2px solid;
149 | box-sizing: border-box; }
150 | .Cal__Day__range.Cal__Day__selected.Cal__Day__end .Cal__Day__selection .Cal__Day__day {
151 | top: 4px; }
152 | .Cal__Container__root {
153 | position: relative;
154 | display: table;
155 | z-index: 1;
156 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
157 | line-height: 1.4em;
158 | -webkit-font-smoothing: antialiased;
159 | -moz-osx-font-smoothing: grayscale;
160 | font-weight: 300;
161 | outline: none;
162 | text-align: left; }
163 | .Cal__Container__root.Cal__Container__landscape {
164 | display: flex;
165 | flex-direction: row; }
166 | .Cal__Container__root.Cal__Container__landscape .Cal__Container__wrapper {
167 | position: relative;
168 | flex-grow: 1;
169 | overflow: hidden;
170 | z-index: 1;
171 | border-top-right-radius: 3px;
172 | border-bottom-right-radius: 3px; }
173 |
174 | .Cal__Container__listWrapper {
175 | position: relative;
176 | overflow: hidden;
177 | background-color: #FFF; }
178 | .Cal__Header__root {
179 | position: relative;
180 | display: flex;
181 | align-items: center;
182 | box-sizing: border-box;
183 | overflow: hidden;
184 | min-height: 98px;
185 | padding: 20px;
186 | line-height: 1.3;
187 | font-weight: 400;
188 | border-top-left-radius: 3px;
189 | border-top-right-radius: 3px; }
190 | .Cal__Header__root.Cal__Header__landscape {
191 | align-items: flex-start;
192 | min-width: 200px;
193 | border-top-right-radius: 0;
194 | border-bottom-left-radius: 3px; }
195 | .Cal__Header__root.Cal__Header__landscape .Cal__Header__dateWrapper.Cal__Header__day {
196 | flex-grow: 1;
197 | height: 76px; }
198 |
199 | .Cal__Header__wrapper {
200 | display: flex;
201 | flex-direction: column;
202 | flex-grow: 1;
203 | cursor: pointer; }
204 | .Cal__Header__wrapper.Cal__Header__blank {
205 | height: 58px;
206 | line-height: 58px;
207 | color: rgba(255, 255, 255, 0.5);
208 | font-size: 18px;
209 | cursor: default; }
210 |
211 | .Cal__Header__dateWrapper {
212 | position: relative;
213 | display: block;
214 | overflow: hidden;
215 | color: rgba(255, 255, 255, 0.5);
216 | transition: color 0.3s ease; }
217 | .Cal__Header__dateWrapper.Cal__Header__active {
218 | color: white; }
219 | .Cal__Header__dateWrapper.Cal__Header__day {
220 | height: 38px;
221 | font-size: 36px;
222 | line-height: 36px;
223 | text-transform: capitalize; }
224 | .Cal__Header__dateWrapper.Cal__Header__year {
225 | height: 20px;
226 | font-size: 18px;
227 | line-height: 18px; }
228 |
229 | .Cal__Header__date {
230 | position: absolute;
231 | top: 0;
232 | left: 0;
233 | right: 0;
234 | bottom: 0; }
235 |
236 | .Cal__Header__range {
237 | display: flex;
238 | flex-grow: 1; }
239 | .Cal__Header__range .Cal__Header__dateWrapper {
240 | overflow: visible; }
241 | .Cal__Header__range .Cal__Header__wrapper:first-child:before, .Cal__Header__range .Cal__Header__wrapper:first-child:after {
242 | content: '';
243 | position: absolute;
244 | top: 0;
245 | left: 50%;
246 | width: 0;
247 | height: 0;
248 | margin-top: -50px;
249 | margin-left: -50px;
250 | border-top: 100px solid transparent;
251 | border-bottom: 100px solid transparent;
252 | border-left: 60px solid; }
253 | .Cal__Header__range .Cal__Header__wrapper:first-child:before {
254 | color: rgba(255, 255, 255, 0.15);
255 | transform: translateX(1px); }
256 | .Cal__Header__range .Cal__Header__wrapper:last-child {
257 | margin-left: 60px; }
258 | .Cal__Header__range .Cal__Header__wrapper .Cal__Header__date {
259 | white-space: nowrap;
260 | z-index: 1; }
261 | .Cal__Today__root {
262 | position: absolute;
263 | top: 0;
264 | left: 0;
265 | right: 0;
266 | display: flex;
267 | align-items: center;
268 | justify-content: center;
269 | height: 32px;
270 | padding: 6px;
271 | box-sizing: border-box;
272 | transform: translate3d(0, -100%, 0);
273 | font-weight: 500;
274 | line-height: 0;
275 | z-index: 10;
276 | cursor: pointer;
277 | transition: transform 0.3s ease;
278 | transition-delay: 0.3s; }
279 | .Cal__Today__root.Cal__Today__show {
280 | transform: translate3d(0, 0, 0);
281 | transition-delay: 0s; }
282 | .Cal__Today__root.Cal__Today__show .Cal__Today__chevron {
283 | transition: transform 0.3s ease; }
284 | .Cal__Today__root .Cal__Today__chevron {
285 | position: absolute;
286 | top: 50%;
287 | margin-top: -6px;
288 | margin-left: 5px;
289 | transform: rotate(270deg);
290 | transition: transform 0.3s ease; }
291 | .Cal__Today__root.Cal__Today__chevronUp .Cal__Today__chevron {
292 | transform: rotate(180deg); }
293 | .Cal__Today__root.Cal__Today__chevronDown .Cal__Today__chevron {
294 | transform: rotate(360deg); }
295 | .Cal__MonthList__root {
296 | width: 100% !important;
297 | background-color: #FFF;
298 | -webkit-overflow-scrolling: touch; }
299 | .Cal__MonthList__root.Cal__MonthList__scrolling > div {
300 | pointer-events: none; }
301 | .Cal__MonthList__root.Cal__MonthList__scrolling label {
302 | opacity: 1; }
303 | .Cal__Weekdays__root {
304 | position: relative;
305 | z-index: 5;
306 | display: flex;
307 | padding: 0;
308 | margin: 0;
309 | list-style: none;
310 | box-shadow: inset 0 -1px rgba(0, 0, 0, 0.04); }
311 |
312 | .Cal__Weekdays__day {
313 | padding: 15px 0;
314 | flex-basis: 14.28571%;
315 | flex-grow: 1;
316 | font-weight: 500;
317 | text-align: center; }
318 | .Cal__Years__root {
319 | position: absolute;
320 | left: 0;
321 | right: 0;
322 | bottom: 0;
323 | z-index: 10;
324 | display: flex;
325 | align-items: center;
326 | justify-content: center;
327 | background-color: #F9F9F9; }
328 | .Cal__Years__root:before, .Cal__Years__root:after {
329 | content: '';
330 | position: absolute;
331 | left: 0;
332 | right: 0;
333 | height: 50px;
334 | pointer-events: none;
335 | z-index: 1; }
336 | .Cal__Years__root:before {
337 | top: 0;
338 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%); }
339 | .Cal__Years__root:after {
340 | bottom: 0;
341 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%); }
342 |
343 | .Cal__Years__list {
344 | box-sizing: border-box; }
345 | .Cal__Years__list.Cal__Years__center {
346 | display: flex;
347 | align-items: center;
348 | justify-content: center; }
349 |
350 | .Cal__Years__year {
351 | display: flex;
352 | padding: 0 20px;
353 | height: 100%;
354 | align-items: center;
355 | justify-content: center;
356 | font-size: 18px;
357 | font-weight: 500;
358 | text-align: center;
359 | cursor: pointer;
360 | -webkit-user-select: none;
361 | box-sizing: border-box; }
362 | .Cal__Years__year.Cal__Years__withMonths {
363 | border-bottom: 1px solid #E9E9E9; }
364 | .Cal__Years__year.Cal__Years__withMonths label {
365 | height: 88px;
366 | padding-top: 12px;
367 | box-sizing: border-box; }
368 | .Cal__Years__year label {
369 | flex-basis: 42%; }
370 | .Cal__Years__year label span {
371 | flex-shrink: 0;
372 | color: #333; }
373 | .Cal__Years__year ol {
374 | display: flex;
375 | flex-wrap: wrap;
376 | margin: 0;
377 | padding: 0;
378 | font-size: 14px; }
379 | .Cal__Years__year ol li {
380 | display: flex;
381 | width: 44px;
382 | height: 44px;
383 | flex-shrink: 0;
384 | align-items: center;
385 | justify-content: center;
386 | list-style: none;
387 | border-radius: 50%;
388 | box-sizing: border-box;
389 | color: #444;
390 | font-weight: 400;
391 | text-transform: capitalize; }
392 | .Cal__Years__year ol li.Cal__Years__currentMonth {
393 | border: 1px solid; }
394 | .Cal__Years__year ol li.Cal__Years__selected {
395 | position: relative;
396 | z-index: 1;
397 | background-color: blue;
398 | color: #FFF !important;
399 | border: 0; }
400 | .Cal__Years__year ol li.Cal__Years__disabled {
401 | cursor: not-allowed;
402 | color: #AAA; }
403 | .Cal__Years__year ol li.Cal__Years__disabled:hover {
404 | background-color: inherit; }
405 | .Cal__Years__year ol li:hover {
406 | background-color: #EFEFEF; }
407 | .Cal__Years__year:hover label > span, .Cal__Years__year.Cal__Years__active label > span {
408 | color: inherit; }
409 | .Cal__Years__year:hover, .Cal__Years__year.Cal__Years__active {
410 | position: relative;
411 | z-index: 2; }
412 | .Cal__Years__year.Cal__Years__active {
413 | font-size: 32px; }
414 | .Cal__Years__year.Cal__Years__currentYear {
415 | position: relative; }
416 | .Cal__Years__year.Cal__Years__currentYear label > span {
417 | min-width: 50px;
418 | padding-bottom: 5px;
419 | border-bottom: 3px solid; }
420 | .Cal__Years__year.Cal__Years__currentYear.Cal__Years__active label > span {
421 | min-width: 85px; }
422 | .Cal__Years__year.Cal__Years__first {
423 | padding-top: 40px; }
424 | .Cal__Years__year.Cal__Years__last {
425 | padding-bottom: 40px; }
426 | .Cal__Animation__enter {
427 | opacity: 0;
428 | transform: translate3d(0, -100%, 0);
429 | transition: 0.25s ease; }
430 |
431 | .Cal__Animation__enter.Cal__Animation__enterActive {
432 | opacity: 1;
433 | transform: translate3d(0, 0, 0); }
434 |
435 | .Cal__Animation__leave {
436 | transform: translate3d(0, 0, 0);
437 | transition: 0.25s ease; }
438 |
439 | .Cal__Animation__leave.Cal__Animation__leaveActive {
440 | opacity: 0;
441 | transform: translate3d(0, 100%, 0); }
442 | .Cal__Slider__root, .Cal__Slider__slide {
443 | position: absolute;
444 | top: 0;
445 | left: 0;
446 | right: 0;
447 | bottom: 0; }
448 |
449 | .Cal__Slider__root {
450 | overflow: hidden; }
451 |
452 | .Cal__Slider__slide {
453 | padding: 20px 65px; }
454 | .Cal__Slider__slide:first-child {
455 | padding-left: 20px; }
456 |
457 | .Cal__Slider__wrapper {
458 | height: 100%;
459 | transition: transform 0.3s ease; }
460 |
461 | .Cal__Slider__arrow {
462 | position: absolute;
463 | top: 0;
464 | z-index: 1;
465 | display: flex;
466 | align-items: center;
467 | justify-content: center;
468 | width: 40px;
469 | height: 100%;
470 | opacity: 0.7;
471 | cursor: pointer;
472 | border-left: 1px solid rgba(255, 255, 255, 0.1); }
473 | .Cal__Slider__arrow svg {
474 | width: 15px; }
475 | .Cal__Slider__arrow:hover {
476 | opacity: 1; }
477 |
478 | .Cal__Slider__arrowRight {
479 | right: 0; }
480 |
481 | .Cal__Slider__arrowLeft {
482 | left: 0;
483 | transform: scaleX(-1); }
484 | .Cal__transition__enter {
485 | opacity: 0; }
486 |
487 | .Cal__transition__enterActive {
488 | opacity: 1;
489 | transition: opacity 0.3s ease; }
490 |
491 | .Cal__transition__leave {
492 | opacity: 1; }
493 |
494 | .Cal__transition__leaveActive {
495 | opacity: 0;
496 | transition: opacity 0.3s ease; }
497 | .Cal__Month__rows {
498 | position: relative;
499 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 50%, rgba(0, 0, 0, 0.05) 100%); }
500 |
501 | .Cal__Month__row {
502 | padding: 0;
503 | margin: 0; }
504 | .Cal__Month__row:first-child {
505 | text-align: right; }
506 | .Cal__Month__row:first-child li {
507 | background-color: #FFF;
508 | box-shadow: 0 -1px 0 #E9E9E9; }
509 | .Cal__Month__row:nth-child(2) {
510 | box-shadow: 0 -1px 0 #E9E9E9; }
511 | .Cal__Month__row.Cal__Month__partial:first-child li:first-child {
512 | box-shadow: 0px -1px 0 #E9E9E9, inset 1px 0 0 #E9E9E9; }
513 | .Cal__Month__row.Cal__Month__partial:last-of-type li {
514 | position: relative;
515 | z-index: 1; }
516 |
517 | .Cal__Month__label {
518 | position: absolute;
519 | top: 0;
520 | bottom: 0;
521 | left: 0;
522 | right: 0;
523 | margin: 0;
524 | font-size: 30px;
525 | font-weight: 500;
526 | z-index: 3;
527 | pointer-events: none;
528 | background-color: rgba(255, 255, 255, 0.6);
529 | opacity: 0;
530 | transition: opacity 0.3s ease; }
531 | .Cal__Month__label > span {
532 | position: absolute;
533 | left: 0;
534 | right: 0;
535 | top: 0;
536 | bottom: 56px;
537 | display: flex;
538 | align-items: center;
539 | justify-content: center;
540 | text-transform: capitalize; }
541 | .Cal__Month__label.Cal__Month__partialFirstRow {
542 | top: 56px; }
543 |
--------------------------------------------------------------------------------