├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .huskyrc ├── .npmignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── styles │ ├── _mixin.scss │ ├── _variable.scss │ ├── app.scss │ ├── calendar.scss │ └── theme │ └── red.scss ├── examples └── CalendarSelectedController.tsx ├── package.json ├── src ├── common │ ├── @types.ts │ └── Constant.ts ├── components │ ├── Backdrop.tsx │ ├── Calendar.tsx │ ├── CalendarBody.tsx │ ├── CalendarContainer.tsx │ ├── CalendarHead.tsx │ ├── DatePicker.tsx │ ├── DayView.tsx │ ├── Picker.tsx │ ├── PickerInput.tsx │ ├── RangeDatePicker.tsx │ ├── RangePickerInput.tsx │ ├── SVGIcon │ │ ├── IconBase.tsx │ │ ├── Icons.tsx │ │ ├── SVGIcon.tsx │ │ └── index.tsx │ ├── TableCell.tsx │ ├── TableMatrixView.tsx │ ├── TimeContainer.tsx │ ├── TimeInput.tsx │ └── TodayPanel.tsx ├── index.ts └── utils │ ├── ArrayUtil.ts │ ├── DOMUtil.ts │ ├── DateUtil.ts │ ├── FunctionUtil.ts │ ├── LocaleUtil.ts │ ├── StringUtil.ts │ └── TypeUtil.ts ├── stories ├── Calendar.stories.tsx ├── DatePicker.stories.tsx ├── PickerInput.stories.tsx ├── RangeDatePicker.stories.tsx ├── TimeContainer.stories.tsx ├── TimeInput.stories.tsx ├── css │ └── custom.css └── decorator │ └── LayoutDecorator.tsx ├── test-preprocessor.js ├── test-setup.js ├── test-shim.js ├── test ├── ArrayUtil.test.ts ├── Calendar.test.tsx ├── CalendarBody.test.tsx ├── CalendarContainer.test.tsx ├── CalendarHead.test.tsx ├── DOMUtil.test.ts ├── DatePicker.test.tsx ├── DateUtil.test.ts ├── DayView.test.tsx ├── LocaleUtil.test.ts ├── Picker.test.tsx ├── PickerInput.test.tsx ├── RangeDatePicker.test.tsx ├── RangePickerInput.test.tsx ├── StringUtil.test.ts ├── TableCell.test.tsx ├── TableMatrixView.test.tsx ├── TimeContainer.test.tsx ├── TimeInput.test.tsx ├── TodayPanel.test.tsx ├── __snapshots__ │ ├── Calendar.test.tsx.snap │ ├── CalendarBody.test.tsx.snap │ ├── CalendarContainer.test.tsx.snap │ ├── CalendarHead.test.tsx.snap │ ├── DayView.test.tsx.snap │ ├── RangeDatePicker.test.tsx.snap │ ├── RangePickerInput.test.tsx.snap │ ├── TableCell.test.tsx.snap │ ├── TableMatrixView.test.tsx.snap │ └── TodayPanel.test.tsx.snap └── utils │ └── TestingUtil.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## System and generated files 2 | .idea/ 3 | .DS_Store 4 | .vscode/ 5 | .sass-cache 6 | .jest.test.result.json 7 | 8 | ## Directories 9 | log/ 10 | dist/ 11 | node_modules/ 12 | coverage/ 13 | bower_components/ 14 | storybook-static/ 15 | demo/ 16 | lib/ 17 | 18 | ## Files 19 | result.xml 20 | *.log 21 | out*/ -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "npx lint-staged" 4 | } 5 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | examples 3 | node_modules 4 | src 5 | .babelrc 6 | tsconfig.json 7 | tslint.json 8 | webpack.config.build.js 9 | webpack.config.dev.js 10 | webpack.config.js 11 | yarn-error.log 12 | yarn.lock 13 | .storybook 14 | .idea 15 | .vscode 16 | coverage 17 | examples 18 | stories 19 | storybook-static 20 | test 21 | .jest.test.result.json 22 | .travis.yml 23 | test-*.js 24 | webpack*.js 25 | yarn-error.log 26 | assets/images 27 | dist 28 | out*/ -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register'; 2 | import '@storybook/addon-actions/register'; 3 | import '@storybook/addon-jest/register'; 4 | import '@storybook/addon-options/register'; 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addDecorator, configure } from '@storybook/react'; 2 | import { withTests } from '@storybook/addon-jest'; 3 | import results from '../.jest.test.result.json'; 4 | import { withKnobs } from '@storybook/addon-knobs'; 5 | import { withInfo } from '@storybook/addon-info'; 6 | import { withOptions } from '@storybook/addon-options'; 7 | import { themes } from '@storybook/components'; 8 | import 'assets/styles/calendar.scss'; 9 | // automatically import all files ending in *.stories.js 10 | const req = require.context('../stories', true, /.stories.tsx$/); 11 | function loadStories() { 12 | req.keys().forEach(filename => req(filename)); 13 | } 14 | 15 | addDecorator( 16 | withOptions({ 17 | name: 'React Datepicker', 18 | url: 'https://github.com/y0c/react-datepicker', 19 | addonPanelInRight: true, 20 | }) 21 | ); 22 | 23 | addDecorator( 24 | withTests({ 25 | results, 26 | filesExt: '.test.tsx', 27 | }) 28 | ); 29 | 30 | addDecorator(withKnobs); 31 | addDecorator( 32 | withInfo({ 33 | inline: true, 34 | }) 35 | ); 36 | 37 | configure(loadStories, module); 38 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // load the default config generator. 2 | const path = require('path'); 3 | 4 | module.exports = ({ config }) => { 5 | // Extend it as you need. 6 | // For example, add typescript loader: 7 | config.module.rules.push({ 8 | test: /\.(ts|tsx)$/, 9 | use: [ 10 | require.resolve('awesome-typescript-loader'), 11 | require.resolve('react-docgen-typescript-loader'), 12 | ], 13 | }); 14 | 15 | config.module.rules.push({ 16 | test: /\.scss$/, 17 | use: [ 18 | require.resolve('style-loader'), 19 | require.resolve('css-loader'), 20 | require.resolve('sass-loader'), 21 | ], 22 | }); 23 | 24 | config.resolve.modules.push(path.join(__dirname, '../')); 25 | config.resolve.extensions.push('.ts', '.tsx'); 26 | return config; 27 | }; 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | 5 | before_install: 6 | - npm i -g npm@latest 7 | - npm install codecov -g 8 | 9 | script: 10 | - yarn test 11 | 12 | after_success: 13 | - codecov 14 | - bash <(curl -s https://codecov.io/bash) 15 | 16 | cache: yarn -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at holnet1026@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Thanks for your interest in improving react-datepicker! 4 | 5 | This repo uses yarn workspaces, so you should install yarn@1.3.2 or higher as a package manager. 6 | 7 | * The development environment is Node 9+. 8 | * Unit tests run with jest 9 | * Code has 100% test coverage and it should stay so (yarn test --coverage to check it) 10 | * Code must be linted to pass our CI (yarn lint) 11 | 12 | ## Coding Convention 13 | 14 | * We use prettier for code styling. Don't worry about tabs vs spaces, or how to indent your code. 15 | * We use ESlint for all other coding standards. We try to be consistent and helpful. 16 | 17 | ## Commit Message Guide 18 | We are following [Karama Commit Message Guide](http://karma-runner.github.io/3.0/dev/git-commit-msg.html) 19 | * feat (new feature for the user, not a new feature for build script) 20 | * fix (bug fix for the user, not a fix to a build script) 21 | * docs (changes to the documentation) 22 | * style (formatting, missing semi colons, etc; no production code change) 23 | * refactor (refactoring production code, eg. renaming a variable) 24 | * test (adding missing tests, refactoring tests; no production code change) 25 | * chore (updating grunt tasks etc; no production code change) 26 | 27 | ## Start now! 28 | 29 | Pick an issue you would like to fix or a feature you would like to see. good-first-issues are a good place to start. 30 | 31 | 32 | ## Fork this project 33 | ``` 34 | # clone your fork repository 35 | git clone https://github.com/y0c/react-datepicker 36 | # install dependency 37 | yarn 38 | # run storybook local 39 | yarn run storybook 40 | ``` 41 | Then open http://localhost:6006 42 | 43 | ## Create Topic Branch 44 | ``` 45 | # branch naming is (feature/fix)/iss-{issue_nubmer}-{feature_name} 46 | git checkout -b feature/iss-01-feature-name 47 | # if you develop complete? 48 | # commit & pull request 49 | ``` 50 | if your PR pass code review then merge to master branch 51 | 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 HoSung Lim 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 | drawing 4 | 5 | 6 |

7 |

8 | 9 | React DatePicker 10 | 11 |

12 | 13 | 14 |

15 | 16 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) [![npm version](https://badge.fury.io/js/%40y0c%2Freact-datepicker.svg)](https://badge.fury.io/js/%40y0c%2Freact-datepicker) 17 | [![Build Status](https://travis-ci.com/y0c/react-datepicker.svg?branch=master)](https://travis-ci.com/y0c/react-datepicker) 18 | [![codecov](https://codecov.io/gh/y0c/react-datepicker/branch/master/graph/badge.svg)](https://codecov.io/gh/y0c/react-datepicker) 19 | [![Maintainability](https://api.codeclimate.com/v1/badges/e2a0ba59adb7412eae87/maintainability)](https://codeclimate.com/github/y0c/react-datepicker/maintainability) 20 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 21 | [![dependencies Status](https://david-dm.org/y0c/react-datepicker/status.svg)](https://david-dm.org/y0c/react-datepicker) 22 | [![devDependencies Status](https://david-dm.org/y0c/react-datepicker/dev-status.svg)](https://david-dm.org/y0c/react-datepicker?type=dev) 23 | [![Storybook](https://github.com/storybooks/brand/blob/master/badge/badge-storybook.svg)](https://y0c.github.io/react-datepicker) 24 | [![NPM Download](https://img.shields.io/npm/dt/@y0c/react-datepicker.svg?style=flat)](https://www.npmjs.com/package/@y0c/react-datepicker) 25 | [![Join the chat at https://gitter.im/react-datepicker/community](https://badges.gitter.im/react-datepicker/community.svg)](https://gitter.im/react-datepicker/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 26 | 27 |

28 | 29 | > Flexible, Reusable, Mobile friendly DatePicker Component 30 | 31 | ## 🎬 Intro 32 | 33 | ### DatePicker 34 | 35 | ![datepicker](https://user-images.githubusercontent.com/2585676/52909193-a8992400-32c7-11e9-9266-7735c0e6e705.gif) 36 | 37 | 38 | ### RangeDatePicker 39 | 40 | ![rangedatepicker](https://user-images.githubusercontent.com/2585676/52909117-d7ae9600-32c5-11e9-902a-4df671e82611.gif) 41 | 42 | 43 | [Demo in Storybook](https://y0c.github.io/react-datepicker) 44 | 45 | [![Edit React Datepicker](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/pw6n17pk57) 46 | 47 | ## ✨ Major Component 48 | 49 | * RangeDatePicker 50 | * DatePicker 51 | * Standalone Calendar 52 | 53 | The components that you can use are as follows: If you want to configure the `DatePicker` yourself, you can configure it any way you want through the `Default Calendar component`. 54 | 55 | ## 🔧 Built With 56 | 57 | * TypeScript 58 | * Sass 59 | * React 60 | 61 | ## 📦 Dependency 62 | 63 | * Moment.js 64 | 65 | In previous versions, moment.js were used. but now it is changed to `Day.js` to because of bundle size issue (#14) 66 | 67 | * [Day.js](https://github.com/iamkun/dayjs) 68 | 69 | `Day.js` is a javascript library for Parse, validate, manipulate, and display dates and times. this component use `Day.js` library to globalize and control date. You can check the locale list through this [link](https://github.com/iamkun/dayjs/tree/dev/src/locale). 70 | 71 | ## 📲 Installation 72 | 73 | ```sh 74 | yarn add @y0c/react-datepicker 75 | # or 76 | npm install --save @y0c/react-datepicker 77 | ``` 78 | 79 | ## 💡 Examples 80 | 81 | ### Simple DatePicker 82 | 83 | ```javascript 84 | // import Calendar Component 85 | import React, { Component } from 'react'; 86 | import { DatePicker } from '@y0c/react-datepicker'; 87 | // import calendar style 88 | // You can customize style by copying asset folder. 89 | import '@y0c/react-datepicker/assets/styles/calendar.scss'; 90 | 91 | class DatePickerExample extends Component { 92 | 93 | onChange = (date) => { 94 | // Day.js object 95 | console.log(date); 96 | 97 | // to normal Date object 98 | console.log(date.toDate()); 99 | } 100 | 101 | render() { 102 | return ( 103 | 104 | ) 105 | } 106 | } 107 | ``` 108 | 109 | You can find more Exmaples and Demo in story book link 110 | 111 | ## 🌎 i18n 112 | 113 | Features for i18n are provided by Day.js by default. 114 | 115 | see locale list https://github.com/iamkun/dayjs/tree/dev/src/ 116 | 117 | and you can customize the locale object 118 | 119 | ```javascript 120 | // use day.js locale 121 | import 'dayjs/locale/ko' 122 | 123 | // delivery prop locale string 124 | 125 | 126 | // or define customize locale object 127 | const locale = { 128 | name: 'ko', 129 | weekdays: '일요일_월요일_화요일_수요일_목요일_금요일_토요일'.split('_'), 130 | weekdaysShort: '일_월_화_수_목_금_토'.split('_'), 131 | months: '1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월'.split('_'), 132 | }; 133 | 134 | // delivery propr locale object 135 | 136 | ``` 137 | 138 | Defaults locale `en` 139 | 140 | ### 🎨 Themeing 141 | 142 | 1. Copy this project asset folder under scss file 143 | 2. Override scss variable you want(_variable.scss) 144 | ( red theme examples ) 145 | 146 | ```scss 147 | // red_theme.scss 148 | $base-font-size: 12px; 149 | $title-font-size: 1.3em; 150 | 151 | // override scss variable 152 | $primary-color-dark: #e64a19; 153 | $primary-color: #ff5722; 154 | $primary-color-light: #ffccbc; 155 | $primary-color-text: #ffffff; 156 | $accent-color: #ff5252; 157 | $primary-text-color: #212121; 158 | $secondary-text-color: #757575; 159 | $divider-color: #e4e4e4; 160 | $today-bg-color: #fff9c4; 161 | 162 | // import mixin 163 | @import "../node_modules/@y0c/react-datepicker/assets/styles/_mixin.scss"; 164 | // import app scss 165 | // if you want other style customize 166 | // app.scss copy & rewrite ! 167 | @import "../node_modules/@y0c/react-datepicker/assets/styles/app.scss"; 168 | 169 | ``` 170 | 171 | if you want custom css rewrite `app.scss` file 172 | 173 | Try this example! 174 | 175 | [![Edit 1rw1lp8w7j](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/1rw1lp8w7j) 176 | 177 | ## ⚙️ Local Development 178 | 179 | This component is managed by a `storybook` which is combined with `develop environment` and `documentation`. If you want develop in local environment, clone project and develop through a storybook 180 | 181 | ```sh 182 | # clone this project 183 | git clone https://github.com/y0c/react-datepicker.git 184 | # install dependency 185 | yarn 186 | # start storybook 187 | yarn run storybook 188 | ``` 189 | Open your browser and connect http://localhost:6006 190 | 191 | ## 💼 Get Support 192 | 193 | Please fork and use [https://codesandbox.io/s/pw6n17pk57](https://codesandbox.io/s/pw6n17pk57) to reproduce your problem. 194 | 195 | * Open a new issue(Bug or Feature) on [Github](https://github.com/y0c/react-datepicker/issues/new/choose) 196 | * Join the [Gitter room](https://gitter.im/react-datepicker/community) to chat with other developers. 197 | 198 | ## 👨‍👦‍👦 Contribution 199 | 200 | Issue and Pull Request are always welcome! 201 | 202 | ## 📝 License 203 | MIT 204 | 205 | -------------------------------------------------------------------------------- /assets/styles/_mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin inline-center { 2 | display: inline-flex; 3 | align-items: center; 4 | } -------------------------------------------------------------------------------- /assets/styles/_variable.scss: -------------------------------------------------------------------------------- 1 | $base-font-size: 12px; 2 | $title-font-size: 1.3em; 3 | 4 | $primary-color-dark: #49599a; 5 | $primary-color: #7986cb; 6 | $primary-color-light: #aab6fe; 7 | $primary-color-text: #FFFFFF; 8 | $accent-color: #03A9F4; 9 | $primary-text-color: #212121; 10 | $secondary-text-color: #757575; 11 | $divider-color: #e4e4e4; 12 | $today-bg-color: #FFF9C4; 13 | -------------------------------------------------------------------------------- /assets/styles/app.scss: -------------------------------------------------------------------------------- 1 | 2 | @import url('https://fonts.googleapis.com/css?family=Lato'); 3 | 4 | .rc-backdrop { 5 | position: fixed; 6 | top:0; 7 | left:0; 8 | bottom:0; 9 | right:0; 10 | z-index:80; 11 | &.invert { 12 | background: rgba(1,1,1,.7) 13 | } 14 | } 15 | 16 | .range-picker-input { 17 | display: inline-flex; 18 | border: 1px solid $divider-color; 19 | width: 300px; 20 | * { 21 | box-sizing: border-box; 22 | } 23 | &__icon { 24 | display: inline-flex; 25 | align-items: center; 26 | } 27 | &__start, &__end { 28 | display: inline-flex; 29 | flex: 1; 30 | .picker-input.range { 31 | input { 32 | width: 100%; 33 | border: none; 34 | } 35 | } 36 | } 37 | } 38 | 39 | .picker-input { 40 | display: inline-block; 41 | position: relative; 42 | &__icon { 43 | position:absolute; 44 | top: 50%; 45 | transform: translateY(-50%); 46 | left: 10px; 47 | @include inline-center 48 | } 49 | &__text { 50 | padding: 10px; 51 | border: 1px solid $divider-color; 52 | outline: none; 53 | font-size: $base-font-size * 1.4; 54 | &:disabled { 55 | background: $divider-color; 56 | } 57 | } 58 | &__clear { 59 | position:absolute; 60 | top: 50%; 61 | transform: translateY(-50%); 62 | right: 10px; 63 | cursor: pointer; 64 | } 65 | } 66 | 67 | .picker { 68 | display: inline-block; 69 | &__container { 70 | position: absolute; 71 | z-index:100; 72 | &.portal { 73 | position: fixed; 74 | top: 50%; 75 | left: 50%; 76 | transform: translateX(-50%) translateY(-50%); 77 | } 78 | &__include-time { 79 | border: 1px solid $divider-color; 80 | .calendar__item, 81 | .time__container { 82 | border: none; 83 | } 84 | } 85 | &__tab { 86 | & button { 87 | padding: 5px 10px; 88 | outline: none; 89 | display: inline-flex; 90 | align-items: center; 91 | background: none; 92 | border:none; 93 | border-bottom: 2px solid $divider-color; 94 | &.active { 95 | color: $primary-color-dark; 96 | border-bottom: 2px solid $primary-color-dark; 97 | } 98 | &:first-child { 99 | border-right: none; 100 | } 101 | svg { 102 | margin-right: 5px; 103 | } 104 | } 105 | margin: 10px 0; 106 | } 107 | } 108 | } 109 | 110 | 111 | .time__container { 112 | display: inline-flex; 113 | align-items: center; 114 | border: 1px solid $divider-color; 115 | padding: 15px; 116 | background: white; 117 | font-family: 'Lato'; 118 | font-size: $base-font-size; 119 | &__div { 120 | margin: 0 10px; 121 | } 122 | &__type { 123 | display: flex; 124 | flex-direction: column; 125 | margin-left: 10px; 126 | } 127 | } 128 | 129 | .time-input { 130 | display: inline-block; 131 | width: 40px; 132 | overflow: hidden; 133 | &__up, &__down { 134 | border: 1px solid $divider-color; 135 | button { 136 | outline: none; 137 | width: 100%; 138 | cursor: pointer; 139 | border: none; 140 | } 141 | } 142 | 143 | &__text { 144 | width: 100%; 145 | border-left: 1px solid $divider-color; 146 | border-right: 1px solid $divider-color; 147 | box-sizing: border-box; 148 | input { 149 | width: 100%; 150 | box-sizing: border-box; 151 | border: none; 152 | font-size: 15px; 153 | padding: 5px; 154 | text-align: center; 155 | outline: none; 156 | } 157 | } 158 | 159 | } 160 | 161 | .calendar{ 162 | display:inline-block; 163 | background: white; 164 | font-size: $base-font-size; 165 | *, *:before, *:after { 166 | box-sizing: border-box; 167 | } 168 | 169 | &__container { 170 | width: 270px; 171 | font-family: 'Roboto', sans-serif; 172 | display:none; 173 | } 174 | 175 | &__list { 176 | display:table; 177 | } 178 | 179 | &__item { 180 | display: table-cell; 181 | border: 1px solid lighten($divider-color,3%); 182 | &:not(:first-child) { 183 | border-left: none !important; 184 | } 185 | } 186 | 187 | &--show { 188 | display:inline-block; 189 | } 190 | 191 | &__head { 192 | position:relative; 193 | background: $primary-color; 194 | padding: 10px 6px; 195 | &--title { 196 | font-size: $title-font-size; 197 | color: white; 198 | text-align: center; 199 | margin: 4px; 200 | } 201 | &--button{ 202 | outline: none; 203 | border: none; 204 | cursor: pointer; 205 | background: none; 206 | font-size: 20px; 207 | svg { 208 | fill: white; 209 | } 210 | } 211 | &--prev, &--next { 212 | position: absolute; 213 | top: 0; 214 | bottom: 0; 215 | display: flex; 216 | align-items: center; 217 | } 218 | &--prev { 219 | left:0; 220 | } 221 | &--next { 222 | right:0; 223 | } 224 | } 225 | 226 | &__panel { 227 | &--show { 228 | display: block !important; 229 | } 230 | 231 | &--today { 232 | background: $primary-color-light; 233 | padding: 5px; 234 | display:none; 235 | h2 { 236 | margin: 0; 237 | cursor: pointer; 238 | font-size: $base-font-size; 239 | text-align: center; 240 | } 241 | } 242 | } 243 | 244 | &__body { 245 | &--table{ 246 | width: 100%; 247 | table-layout:fixed; 248 | text-align: center; 249 | border-spacing: none; 250 | border-collapse: collapse; 251 | th { 252 | height: 30px; 253 | vertical-align: middle; 254 | color: $primary-text-color; 255 | } 256 | } 257 | } 258 | 259 | &__day { 260 | vertical-align: top; 261 | padding-top:5px; 262 | height: 40px; 263 | &:hover:not(&--disabled) { 264 | background: $primary-color-light; 265 | cursor: pointer; 266 | } 267 | cursor:pointer; 268 | &--0 { color:red; } 269 | &--6 { color:blue; } 270 | &--today{ background: $today-bg-color; } 271 | &--disabled { color: #ddd; cursor: initial} 272 | &--start, &--end, &--selected { 273 | background: $primary-color; 274 | color: $primary-color-text; 275 | &:hover { 276 | background: $primary-color; 277 | } 278 | } 279 | &--range { background: lighten($primary-color-light,10%); } 280 | &--text{ 281 | display: block; 282 | font-size: 10px; 283 | } 284 | } 285 | 286 | &__year, &__month { 287 | height: 55px; 288 | vertical-align: middle; 289 | &:hover { 290 | background: $primary-color-light; 291 | cursor: pointer; 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /assets/styles/calendar.scss: -------------------------------------------------------------------------------- 1 | @import "_variable.scss"; 2 | @import "_mixin.scss"; 3 | @import "app.scss"; -------------------------------------------------------------------------------- /assets/styles/theme/red.scss: -------------------------------------------------------------------------------- 1 | $base-font-size: 12px; 2 | $title-font-size: 1.3em; 3 | 4 | $primary-color-dark: #49599a; 5 | $primary-color: red; 6 | $primary-color-light: #aab6fe; 7 | $primary-color-text: #FFFFFF; 8 | $accent-color: #03A9F4; 9 | $primary-text-color: #212121; 10 | $secondary-text-color: #757575; 11 | $divider-color: #e4e4e4; 12 | $today-bg-color: #FFF9C4; 13 | 14 | @import "../_mixin.scss"; 15 | @import "../app.scss"; 16 | -------------------------------------------------------------------------------- /examples/CalendarSelectedController.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as dayjs from 'dayjs'; 3 | import Calendar, { Props as ICalendarProps } from '../src/components/Calendar'; 4 | import { Omit, Merge } from '../src/utils/TypeUtil'; 5 | 6 | type CalendarProps = Merge< 7 | Omit, 8 | { 9 | /** showMonth count at once */ 10 | showMonthCnt?: number; 11 | } 12 | >; 13 | 14 | interface IProps { 15 | multiple?: boolean; 16 | } 17 | 18 | interface State { 19 | selected: dayjs.Dayjs[]; 20 | } 21 | 22 | type Props = CalendarProps & IProps; 23 | class CalendarSelectedController extends React.Component { 24 | public static defaultProps = { 25 | multiple: false, 26 | }; 27 | 28 | public state = { 29 | selected: [], 30 | }; 31 | 32 | public handleChange = (date: dayjs.Dayjs) => { 33 | const { multiple } = this.props; 34 | this.setState({ 35 | selected: multiple ? [...this.state.selected, date] : [date], 36 | }); 37 | }; 38 | 39 | public handleClear = () => { 40 | this.setState({ 41 | selected: [], 42 | }); 43 | }; 44 | 45 | public render() { 46 | const { selected } = this.state; 47 | return ( 48 |
49 | 50 | {this.props.multiple && } 51 |
52 | ); 53 | } 54 | } 55 | 56 | export default CalendarSelectedController; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@y0c/react-datepicker", 3 | "version": "1.0.4", 4 | "description": "Flexible, Reusable, Mobile friendly DatePicker Component", 5 | "author": "y0c", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "types": "lib/index.d.ts", 9 | "homepage": "https://github.com/y0c/react-datepicker", 10 | "scripts": { 11 | "dev": "cross-env NODE_ENV=dev webpack-dev-server --progress --mode development --config webpack.config.dev.js", 12 | "build": "npx tsc", 13 | "test": "npx cross-env TZ=Asia/Seoul npx jest", 14 | "test:watch": "yarn test --watch", 15 | "test:coverage": "yarn test --coverage && codecov", 16 | "lint": "tslint --project . --config ./tslint.json", 17 | "storybook": "start-storybook -p 6006", 18 | "build-storybook": "build-storybook", 19 | "bump:patch": "yarn version --patch", 20 | "bump:minor": "yarn version --minor", 21 | "bump:major": "yarn version --major", 22 | "prepublish": "yarn lint && yarn test", 23 | "deploy-storybook": "storybook-to-ghpages", 24 | "test:unit:output": "yarn test --json --outputFile=.jest.test.result.json", 25 | "start": "yarn test:unit:output && start-storybook -p 6006" 26 | }, 27 | "lint-staged": { 28 | "{src}/**/*.ts*": [ 29 | "prettier --write", 30 | "git add" 31 | ] 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://y0c@github.com/y0c/react-datepicker.git" 36 | }, 37 | "dependencies": { 38 | "classnames": "^2.2.6", 39 | "dayjs": "^1.8.21", 40 | "react": "^16.8.4", 41 | "react-dom": "^16.8.4" 42 | }, 43 | "prettier": { 44 | "printWidth": 100, 45 | "parser": "typescript", 46 | "singleQuote": true, 47 | "useTabs": false, 48 | "tabWidth": 2, 49 | "trailingComma": "es5" 50 | }, 51 | "jest": { 52 | "preset": "ts-jest", 53 | "snapshotSerializers": [ 54 | "enzyme-to-json/serializer" 55 | ], 56 | "setupFiles": [ 57 | "/test-shim.js", 58 | "/test-setup.js" 59 | ], 60 | "collectCoverage": true, 61 | "collectCoverageFrom": [ 62 | "**/src/components/**/*.{ts,tsx}", 63 | "**/src/utils/**/*.{ts,tsx}", 64 | "!**/index.ts", 65 | "!**/node_modules/**", 66 | "!**/lib/**", 67 | "!**/examples/**", 68 | "!**/vendor/**" 69 | ], 70 | "moduleFileExtensions": [ 71 | "ts", 72 | "tsx", 73 | "js" 74 | ], 75 | "testMatch": [ 76 | "**/test/*test.(ts|tsx|js)" 77 | ] 78 | }, 79 | "devDependencies": { 80 | "@babel/core": "^7.4.3", 81 | "@emotion/core": "^10.0.10", 82 | "@storybook/addon-actions": "^5.0.6", 83 | "@storybook/addon-info": "^5.0.6", 84 | "@storybook/addon-jest": "^5.0.6", 85 | "@storybook/addon-knobs": "^5.0.6", 86 | "@storybook/addon-links": "^5.0.6", 87 | "@storybook/addon-options": "^5.0.6", 88 | "@storybook/addons": "^5.0.6", 89 | "@storybook/cli": "^5.0.6", 90 | "@storybook/react": "^5.0.6", 91 | "@storybook/storybook-deployer": "^2.8.1", 92 | "@types/classnames": "^2.2.6", 93 | "@types/enzyme": "^3.1.15", 94 | "@types/jest": "^24.0.11", 95 | "@types/lodash": "^4.14.118", 96 | "@types/react": "^16.7.13", 97 | "@types/react-dom": "^16.0.11", 98 | "@types/sinon": "^7.0.3", 99 | "@types/storybook__addon-actions": "^3.4.1", 100 | "@types/storybook__addon-knobs": "^4.0.0", 101 | "@types/storybook__react": "^4.0.0", 102 | "awesome-typescript-loader": "^5.2.1", 103 | "babel-loader": "^8.0.5", 104 | "clean-webpack-plugin": "2.0.1", 105 | "cross-env": "5.2.0", 106 | "css-loader": "2.1.0", 107 | "enzyme": "^3.8.0", 108 | "enzyme-adapter-react-16": "^1.7.1", 109 | "enzyme-to-json": "^3.3.5", 110 | "file-loader": "3.0.1", 111 | "html-webpack-plugin": "3.2.0", 112 | "husky": "^1.3.1", 113 | "jest": "^24.7.0", 114 | "lint-staged": "^8.1.0", 115 | "node-sass": "4.12.0", 116 | "prettier": "^1.15.3", 117 | "prop-types": "^15.6.2", 118 | "react-docgen-typescript-loader": "^3.0.1", 119 | "react-test-renderer": "^16.7.0", 120 | "sass-loader": "7.1.0", 121 | "sinon": "^7.2.2", 122 | "style-loader": "0.23.1", 123 | "ts-jest": "^24.0.1", 124 | "ts-loader": "^5.3.3", 125 | "ts-mockito": "^2.3.1", 126 | "tslint": "^5.12.1", 127 | "tslint-config-airbnb": "^5.11.1", 128 | "tslint-config-prettier": "^1.17.0", 129 | "tslint-react": "^4.0.0", 130 | "typescript": "^3.2.2", 131 | "url-loader": "^1.1.2", 132 | "webpack": "4.28.4", 133 | "webpack-cli": "3.2.1", 134 | "webpack-dev-server": "3.1.14", 135 | "webpack-merge": "4.2.1" 136 | }, 137 | "bugs": { 138 | "url": "https://github.com/y0c/react-datepicker/issues" 139 | }, 140 | "keywords": [ 141 | "react", 142 | "datepicker", 143 | "calendar", 144 | "rangepicker", 145 | "rangedatepicker" 146 | ] 147 | } 148 | -------------------------------------------------------------------------------- /src/common/@types.ts: -------------------------------------------------------------------------------- 1 | export namespace IDatePicker { 2 | export type Locale = any; 3 | export enum PickerDirection { 4 | TOP, 5 | BOTTOM, 6 | } 7 | 8 | export enum ViewMode { 9 | YEAR, 10 | MONTH, 11 | DAY, 12 | } 13 | 14 | export enum TimeType { 15 | AM = 'AM', 16 | PM = 'PM', 17 | } 18 | export interface Position { 19 | left?: string; 20 | top?: string; 21 | bottom?: string; 22 | right?: string; 23 | } 24 | 25 | export interface SVGIconProps { 26 | size?: string; 27 | className?: string; 28 | color?: string; 29 | onClick?: () => void; 30 | style?: {}; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/common/Constant.ts: -------------------------------------------------------------------------------- 1 | export const DatePickerDefaults = { 2 | dateFormat: 'YYYY-MM-DD', 3 | dateTimeFormat: 'YYYY-MM-DD HH:mm A', 4 | timeFormat: 'HH:mm A', 5 | locale: 'en', 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/Backdrop.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as classNames from 'classnames'; 3 | 4 | interface Props { 5 | /** Backdrop background color invert option */ 6 | invert?: boolean; 7 | /** Backdrop show or hide */ 8 | show?: boolean; 9 | /** Backdrop click event */ 10 | onClick?: () => void; 11 | } 12 | 13 | const Backdrop: React.FunctionComponent = ({ invert, show, onClick }) => ( 14 | 15 | {show &&
} 16 | 17 | ); 18 | 19 | export default Backdrop; 20 | -------------------------------------------------------------------------------- /src/components/Calendar.tsx: -------------------------------------------------------------------------------- 1 | import { range } from '../utils/ArrayUtil'; 2 | import * as dayjs from 'dayjs'; 3 | import * as React from 'react'; 4 | import CalendarContainer, { InheritProps as ContainerProps } from './CalendarContainer'; 5 | 6 | export interface Props extends ContainerProps { 7 | /** Calendar Initial Date Parameters */ 8 | base: dayjs.Dayjs; 9 | /** Number of months to show at once */ 10 | showMonthCnt: number; 11 | } 12 | 13 | export interface State { 14 | base: dayjs.Dayjs; 15 | } 16 | 17 | class Calendar extends React.Component { 18 | public static defaultProps = { 19 | base: dayjs(), 20 | showMonthCnt: 1, 21 | showToday: false, 22 | }; 23 | 24 | constructor(props: Props) { 25 | super(props); 26 | this.state = { 27 | base: props.base, 28 | }; 29 | } 30 | 31 | public setBase = (base: dayjs.Dayjs) => { 32 | this.setState({ base }); 33 | }; 34 | 35 | public render() { 36 | const { showMonthCnt } = this.props; 37 | const { base } = this.state; 38 | 39 | return ( 40 |
41 |
42 | {range(showMonthCnt).map(idx => ( 43 |
44 | 52 |
53 | ))} 54 |
55 |
56 | ); 57 | } 58 | } 59 | 60 | export default Calendar; 61 | -------------------------------------------------------------------------------- /src/components/CalendarBody.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as dayjs from 'dayjs'; 3 | import { IDatePicker } from '../common/@types'; 4 | import { DatePickerDefaults } from '../common/Constant'; 5 | import { getMonthMatrix, getYearMatrix } from '../utils/DateUtil'; 6 | import DayView, { Props as DayViewProps } from './DayView'; 7 | import TableCell from './TableCell'; 8 | import TableMatrixView from './TableMatrixView'; 9 | 10 | interface CalendarBodyProps { 11 | /** Calendar viewMode(Year, Month, Day) */ 12 | viewMode: IDatePicker.ViewMode; 13 | /** Calendar current Date */ 14 | current: dayjs.Dayjs; 15 | /** DayClick Event */ 16 | onClick: (value: string) => void; 17 | /** Locale to use */ 18 | locale: IDatePicker.Locale; 19 | } 20 | type Props = DayViewProps & CalendarBodyProps; 21 | 22 | const YEAR_VIEW_CLASS = 'calendar__year'; 23 | const MONTH_VIEW_CLASS = 'calendar__month'; 24 | 25 | const buildMatrixView = ( 26 | matrix: string[][], 27 | className: string, 28 | onClick: (key: number, value: string) => () => void 29 | ) => { 30 | return ( 31 | ( 34 | 35 | )} 36 | /> 37 | ); 38 | }; 39 | 40 | class CalendarBody extends React.Component { 41 | public static defaultProps = { 42 | viewMode: IDatePicker.ViewMode.DAY, 43 | locale: DatePickerDefaults.locale, 44 | }; 45 | 46 | public render() { 47 | const { current, onClick, locale } = this.props; 48 | const viewMap = { 49 | [IDatePicker.ViewMode.YEAR]: buildMatrixView( 50 | getYearMatrix(dayjs(current).year()), 51 | YEAR_VIEW_CLASS, 52 | (_, v) => () => onClick(v) 53 | ), 54 | [IDatePicker.ViewMode.MONTH]: buildMatrixView( 55 | getMonthMatrix(locale), 56 | MONTH_VIEW_CLASS, 57 | (k, _) => () => onClick(String(k)) 58 | ), 59 | [IDatePicker.ViewMode.DAY]: , 60 | }; 61 | 62 | return
{viewMap[this.props.viewMode]}
; 63 | } 64 | } 65 | export default CalendarBody; 66 | -------------------------------------------------------------------------------- /src/components/CalendarContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as classNames from 'classnames'; 2 | import * as dayjs from 'dayjs'; 3 | import * as React from 'react'; 4 | import { IDatePicker } from '../common/@types'; 5 | import CalendarBody from './CalendarBody'; 6 | import CalendarHead from './CalendarHead'; 7 | import { Props as DayViewProps } from './DayView'; 8 | import TodayPanel from './TodayPanel'; 9 | import { ifExistCall } from '../utils/FunctionUtil'; 10 | import { DatePickerDefaults } from '../common/Constant'; 11 | import { getToday } from '../utils/LocaleUtil'; 12 | 13 | interface CalendarContainerProps { 14 | /** Locale to use */ 15 | locale?: IDatePicker.Locale; 16 | /** Calendar Show or Hide */ 17 | show?: boolean; 18 | /** PrevIcon Show or Hide */ 19 | prevIcon?: boolean; 20 | /** NextIcon Show or Hide */ 21 | nextIcon?: boolean; 22 | /** Event for Calendar day click */ 23 | onChange?: (date: dayjs.Dayjs) => void; 24 | /** TodayPanel show or hide */ 25 | showToday?: boolean; 26 | } 27 | 28 | interface PrivateProps { 29 | /** CalendarContainer base prop */ 30 | current: dayjs.Dayjs; 31 | /** Default Date parameter in calendar, which is the parent component */ 32 | base: dayjs.Dayjs; 33 | /** Number of months to show at once */ 34 | showMonthCnt: number; 35 | /** Set Calendar initial Date */ 36 | setBase: (base: dayjs.Dayjs) => void; 37 | } 38 | 39 | export interface State { 40 | viewMode: IDatePicker.ViewMode; 41 | } 42 | 43 | export type InheritProps = DayViewProps & CalendarContainerProps; 44 | export type Props = CalendarContainerProps & DayViewProps & PrivateProps; 45 | 46 | class CalendarContainer extends React.Component { 47 | public static defaultProps = { 48 | current: dayjs(), 49 | show: true, 50 | showMonthCnt: 1, 51 | showToday: false, 52 | locale: DatePickerDefaults.locale, 53 | }; 54 | 55 | public state = { 56 | viewMode: IDatePicker.ViewMode.DAY, 57 | }; 58 | 59 | constructor(props: Props) { 60 | super(props); 61 | } 62 | 63 | public getHeaderTitle = () => { 64 | const { current } = this.props; 65 | const year = dayjs(current).year(); 66 | return { 67 | [IDatePicker.ViewMode.YEAR]: `${year - 4} - ${year + 5}`, 68 | [IDatePicker.ViewMode.MONTH]: `${year}`, 69 | [IDatePicker.ViewMode.DAY]: dayjs(current).format('YYYY.MM'), 70 | }[this.state.viewMode]; 71 | }; 72 | 73 | public handleTitleClick = () => { 74 | const { viewMode } = this.state; 75 | const { showMonthCnt } = this.props; 76 | let changedMode: IDatePicker.ViewMode = viewMode; 77 | 78 | if (viewMode === IDatePicker.ViewMode.MONTH) { 79 | changedMode = IDatePicker.ViewMode.YEAR; 80 | } else if (viewMode === IDatePicker.ViewMode.DAY) { 81 | changedMode = IDatePicker.ViewMode.MONTH; 82 | } 83 | this.setState({ 84 | viewMode: showMonthCnt > 1 ? IDatePicker.ViewMode.DAY : changedMode, 85 | }); 86 | }; 87 | 88 | public handleChange = (value: string) => { 89 | const { viewMode } = this.state; 90 | const { current, onChange, setBase, showMonthCnt, base } = this.props; 91 | if (!value.trim()) return; 92 | if (showMonthCnt > 1) { 93 | const date = dayjs(current) 94 | .date(parseInt(value, 10)) 95 | .toDate(); 96 | ifExistCall(onChange, date); 97 | return; 98 | } 99 | 100 | if (viewMode === IDatePicker.ViewMode.YEAR) { 101 | setBase(dayjs(base).year(parseInt(value, 10))); 102 | this.setState({ 103 | viewMode: IDatePicker.ViewMode.MONTH, 104 | }); 105 | } else if (viewMode === IDatePicker.ViewMode.MONTH) { 106 | setBase(dayjs(base).month(parseInt(value, 10))); 107 | this.setState({ 108 | viewMode: IDatePicker.ViewMode.DAY, 109 | }); 110 | } else { 111 | const date = dayjs(current).date(parseInt(value, 10)); 112 | ifExistCall(onChange, date); 113 | } 114 | }; 115 | 116 | public handleBase = (method: string) => () => { 117 | const { base, setBase } = this.props; 118 | const { viewMode } = this.state; 119 | const date = dayjs(base); 120 | if (viewMode === IDatePicker.ViewMode.YEAR) { 121 | setBase(date[method](10, 'year')); 122 | } else if (viewMode === IDatePicker.ViewMode.MONTH) { 123 | setBase(date[method](1, 'year')); 124 | } else { 125 | setBase(date[method](1, 'month')); 126 | } 127 | }; 128 | 129 | public handleToday = () => { 130 | const { setBase } = this.props; 131 | setBase(dayjs()); 132 | }; 133 | 134 | public renderCalendarHead = () => { 135 | const { prevIcon, nextIcon } = this.props; 136 | return ( 137 | 145 | ); 146 | }; 147 | 148 | public renderTodayPane = () => { 149 | const { showToday, locale = DatePickerDefaults.locale } = this.props; 150 | return ; 151 | }; 152 | 153 | public renderCalendarBody = () => { 154 | const { 155 | customDayClass, 156 | customDayText, 157 | disableDay, 158 | selected, 159 | startDay, 160 | endDay, 161 | onMouseOver, 162 | current, 163 | locale = DatePickerDefaults.locale, 164 | } = this.props; 165 | 166 | return ( 167 | 180 | ); 181 | }; 182 | 183 | public render() { 184 | const { show, showToday } = this.props; 185 | const calendarClass = classNames('calendar__container', { 186 | 'calendar--show': show, 187 | }); 188 | 189 | return ( 190 |
191 | {this.renderCalendarHead()} 192 | {showToday && this.renderTodayPane()} 193 | {this.renderCalendarBody()} 194 |
195 | ); 196 | } 197 | } 198 | 199 | export default CalendarContainer; 200 | -------------------------------------------------------------------------------- /src/components/CalendarHead.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SVGIcon from './SVGIcon'; 3 | 4 | interface Props { 5 | /** Prev button click event */ 6 | onPrev?: () => void; 7 | /** Next button click event */ 8 | onNext?: () => void; 9 | /** Calenar Title Click Event */ 10 | onTitleClick?: () => void; 11 | /** Prev Icon show or Hide */ 12 | prevIcon?: boolean; 13 | /** Next icon show or hide */ 14 | nextIcon?: boolean; 15 | /** Title to show in calendar */ 16 | title?: string; 17 | } 18 | 19 | const defaultProps = { 20 | title: '', 21 | }; 22 | 23 | const CalendarHead: React.FunctionComponent = ({ 24 | onPrev, 25 | onNext, 26 | prevIcon, 27 | nextIcon, 28 | title, 29 | onTitleClick, 30 | }) => { 31 | return ( 32 |
33 |
34 | {prevIcon && ( 35 | 38 | )} 39 |
40 |

41 | {title} 42 |

43 |
44 | {nextIcon && ( 45 | 48 | )} 49 |
50 |
51 | ); 52 | }; 53 | 54 | CalendarHead.defaultProps = defaultProps; 55 | 56 | export default CalendarHead; 57 | -------------------------------------------------------------------------------- /src/components/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as dayjs from 'dayjs'; 3 | import * as customParseFormat from 'dayjs/plugin/customParseFormat'; 4 | import * as CX from 'classnames'; 5 | import Calendar, { Props as ICalendarProps } from './Calendar'; 6 | import TimeContainer from './TimeContainer'; 7 | import Picker, { PickerProps, PickerAction } from './Picker'; 8 | import { Omit, Merge } from '../utils/TypeUtil'; 9 | import { ifExistCall } from '../utils/FunctionUtil'; 10 | import { formatDate } from '../utils/DateUtil'; 11 | import { DatePickerDefaults } from '../common/Constant'; 12 | import PickerInput, { Props as InputProps } from './PickerInput'; 13 | import SVGIcon from './SVGIcon'; 14 | 15 | export enum TabValue { 16 | DATE, 17 | TIME, 18 | } 19 | 20 | interface DatePickerProps { 21 | /** To display input format (dayjs format) */ 22 | dateFormat?: string; 23 | /** include TimePicker true/false */ 24 | includeTime?: boolean; 25 | /** show time only */ 26 | showTimeOnly?: boolean; 27 | /** Initial display date */ 28 | initialDate?: dayjs.Dayjs; 29 | /** Override InputComponent */ 30 | inputComponent?: (props: InputProps) => JSX.Element; 31 | /** DatePicker value change Event */ 32 | onChange?: (date: dayjs.Dayjs, rawValue: string) => void; 33 | /** DatePicker Input default Icon */ 34 | showDefaultIcon: boolean; 35 | } 36 | 37 | export interface State { 38 | tabValue: TabValue; 39 | date?: dayjs.Dayjs; 40 | inputValue: string; 41 | selected: dayjs.Dayjs[]; 42 | } 43 | 44 | type CalendarProps = Merge< 45 | Omit, 46 | { 47 | /** showMonth count at once */ 48 | showMonthCnt?: number; 49 | } 50 | >; 51 | 52 | export type Props = DatePickerProps & Omit & CalendarProps & PickerProps; 53 | 54 | class DatePicker extends React.Component { 55 | public static defaultProps = { 56 | includeTime: false, 57 | showMonthCnt: 1, 58 | locale: DatePickerDefaults.locale, 59 | portal: false, 60 | showDefaultIcon: false, 61 | }; 62 | 63 | constructor(props: Props) { 64 | super(props); 65 | dayjs.extend(customParseFormat); 66 | const { initialDate, includeTime, showTimeOnly } = this.props; 67 | const selected = []; 68 | let date; 69 | 70 | if (initialDate) { 71 | date = initialDate; 72 | selected.push(date); 73 | } 74 | 75 | if (includeTime && showTimeOnly) { 76 | throw new Error('incldueTime & showTimeOnly cannot be used together'); 77 | } 78 | 79 | this.state = { 80 | date, 81 | selected, 82 | tabValue: TabValue.DATE, 83 | inputValue: formatDate(date, this.getDateFormat()), 84 | }; 85 | } 86 | 87 | public getDateFormat() { 88 | const { dateFormat, includeTime, showTimeOnly } = this.props; 89 | 90 | if (!dateFormat) { 91 | if (includeTime) { 92 | return DatePickerDefaults.dateTimeFormat; 93 | } 94 | if (showTimeOnly) { 95 | return DatePickerDefaults.timeFormat; 96 | } 97 | return DatePickerDefaults.dateFormat; 98 | } 99 | return dateFormat; 100 | } 101 | 102 | public handleDateChange = (date: dayjs.Dayjs) => { 103 | const { onChange } = this.props; 104 | const value = dayjs(date).format(this.getDateFormat()); 105 | 106 | ifExistCall(onChange, date, value); 107 | 108 | this.setState({ 109 | ...this.state, 110 | date, 111 | inputValue: value, 112 | selected: [date], 113 | }); 114 | }; 115 | 116 | public handleTimeChange = (hour: number, minute: number) => { 117 | const { onChange } = this.props; 118 | let date = this.state.date; 119 | let selected = this.state.selected; 120 | 121 | if (!date) { 122 | date = dayjs(); 123 | selected = [date]; 124 | } 125 | 126 | date = date.hour(hour).minute(minute); 127 | const inputValue = date.format(this.getDateFormat()); 128 | 129 | ifExistCall(onChange, date, inputValue); 130 | 131 | this.setState({ 132 | ...this.state, 133 | date, 134 | selected, 135 | inputValue, 136 | }); 137 | }; 138 | 139 | public handleInputChange = (e: React.FormEvent) => { 140 | const { onChange } = this.props; 141 | const value = e.currentTarget.value; 142 | 143 | ifExistCall(onChange, value, undefined); 144 | 145 | this.setState({ 146 | ...this.state, 147 | inputValue: e.currentTarget.value, 148 | }); 149 | }; 150 | 151 | public handleInputClear = () => { 152 | const { onChange } = this.props; 153 | 154 | ifExistCall(onChange, '', undefined); 155 | 156 | this.setState({ 157 | ...this.state, 158 | inputValue: '', 159 | }); 160 | }; 161 | 162 | public handleInputBlur = (e: React.FormEvent) => { 163 | const { date } = this.state; 164 | const value = e.currentTarget.value; 165 | const parsedDate = dayjs(value, this.getDateFormat()); 166 | let updateDate: dayjs.Dayjs | undefined; 167 | 168 | updateDate = date; 169 | 170 | if (dayjs(parsedDate).isValid()) { 171 | updateDate = parsedDate; 172 | } 173 | 174 | this.setState({ 175 | ...this.state, 176 | date: updateDate, 177 | inputValue: dayjs(updateDate).format(this.getDateFormat()), 178 | }); 179 | }; 180 | 181 | public renderInputComponent = (): JSX.Element => { 182 | const { inputComponent, readOnly, disabled, clear, autoFocus, showDefaultIcon, placeholder } = this.props; 183 | const { inputValue } = this.state; 184 | const inputProps = { 185 | readOnly, 186 | autoFocus, 187 | disabled, 188 | clear, 189 | placeholder, 190 | onChange: this.handleInputChange, 191 | onClear: this.handleInputClear, 192 | onBlur: this.handleInputBlur, 193 | value: inputValue, 194 | icon: showDefaultIcon ? : undefined 195 | }; 196 | return inputComponent ? inputComponent({ ...inputProps }) : ; 197 | }; 198 | 199 | public handleTab = (val: TabValue) => () => { 200 | this.setState({ 201 | ...this.state, 202 | tabValue: val, 203 | }); 204 | }; 205 | 206 | public renderTabMenu = (): JSX.Element | null => { 207 | const { tabValue } = this.state; 208 | 209 | const renderButton = (type: TabValue, label: string, icon: string) => ( 210 | 220 | ); 221 | return ( 222 |
223 | {renderButton(TabValue.DATE, 'DATE', 'calendar')} 224 | {renderButton(TabValue.TIME, 'TIME', 'time')} 225 |
226 | ); 227 | }; 228 | 229 | public renderCalendar = (actions: PickerAction): JSX.Element | null => { 230 | const { selected, date } = this.state; 231 | return ( 232 | { 236 | this.handleDateChange(e); 237 | actions.hide(); 238 | }} 239 | selected={selected} 240 | /> 241 | ); 242 | }; 243 | 244 | public renderTime = (): JSX.Element | null => { 245 | const date = this.state.date || dayjs(); 246 | 247 | return ( 248 | 249 | ); 250 | }; 251 | 252 | public renderContents = (actions: PickerAction): JSX.Element => { 253 | const { includeTime, showTimeOnly } = this.props; 254 | const { tabValue } = this.state; 255 | let component: JSX.Element; 256 | 257 | component =
{this.renderCalendar(actions)}
; 258 | 259 | if (showTimeOnly) { 260 | component =
{this.renderTime()}
; 261 | } 262 | 263 | if (includeTime) { 264 | component = ( 265 |
266 | {this.renderTabMenu()} 267 | {tabValue === TabValue.DATE ? this.renderCalendar(actions) : this.renderTime()} 268 |
269 | ); 270 | } 271 | return component; 272 | }; 273 | 274 | public render() { 275 | const { includeTime, portal, direction, disabled, readOnly } = this.props; 276 | 277 | return ( 278 | this.renderInputComponent()} 285 | renderContents={({ actions }) => this.renderContents(actions)} 286 | /> 287 | ); 288 | } 289 | } 290 | 291 | export default DatePicker; 292 | -------------------------------------------------------------------------------- /src/components/DayView.tsx: -------------------------------------------------------------------------------- 1 | import * as classNames from 'classnames'; 2 | import * as dayjs from 'dayjs'; 3 | import * as React from 'react'; 4 | import { DatePickerDefaults } from '../common/Constant'; 5 | import TableCell from './TableCell'; 6 | import TableMatrixView from './TableMatrixView'; 7 | import { ifExistCall } from '../utils/FunctionUtil'; 8 | import { getWeekDays } from '../utils/LocaleUtil'; 9 | 10 | import { getDayMatrix, isDayEqual, isDayRange } from '../utils/DateUtil'; 11 | import { IDatePicker } from '../common/@types'; 12 | 13 | export interface Props { 14 | /** Selected days to show in calendar */ 15 | selected?: dayjs.Dayjs[]; 16 | /** Start day to show in calendar */ 17 | startDay?: dayjs.Dayjs; 18 | /** End day to show in calendar */ 19 | endDay?: dayjs.Dayjs; 20 | /** Calendar day click Event */ 21 | onClick?: (date: string) => void; 22 | /** Calendar day Mouseover Event */ 23 | onMouseOver?: (date: dayjs.Dayjs) => void; 24 | /** Custom day class to show in day */ 25 | customDayClass?: (date: dayjs.Dayjs) => string | string[]; 26 | /** Custom day text to show in day */ 27 | customDayText?: (date: dayjs.Dayjs) => string; 28 | /** Calendar day disable */ 29 | disableDay?: (date: dayjs.Dayjs) => void; 30 | } 31 | 32 | interface PrivateProps { 33 | current: dayjs.Dayjs; 34 | locale: IDatePicker.Locale; 35 | } 36 | 37 | class DayView extends React.Component { 38 | public static defaultProps = { 39 | locale: DatePickerDefaults.locale, 40 | }; 41 | 42 | public getDayClass = (date: string): string => { 43 | const { current, customDayClass, startDay, endDay, selected, disableDay } = this.props; 44 | const currentDate = dayjs(current).date(parseInt(date, 10)); 45 | 46 | let classArr: string[] = []; 47 | 48 | if (!date.trim()) { 49 | return ''; 50 | } 51 | 52 | if (customDayClass !== undefined) { 53 | const customClass = customDayClass(currentDate); 54 | classArr = classArr.concat(typeof customClass === 'string' ? [customClass] : customClass); 55 | } 56 | 57 | const dayClass = classNames( 58 | 'calendar__day', 59 | `calendar__day--${dayjs(currentDate).day()}`, 60 | classArr, 61 | { 62 | 'calendar__day--end': isDayEqual(currentDate, endDay), 63 | 'calendar__day--range': isDayRange(currentDate, startDay, endDay), 64 | 'calendar__day--selected': this.isIncludeDay(date, selected), 65 | 'calendar__day--disabled': disableDay ? disableDay(currentDate) : false, 66 | 'calendar__day--start': isDayEqual(currentDate, startDay), 67 | 'calendar__day--today': isDayEqual(currentDate, dayjs()), 68 | } 69 | ); 70 | 71 | return dayClass; 72 | }; 73 | 74 | public getCustomText = (date: string): string => { 75 | const { current, customDayText } = this.props; 76 | const currentDate = dayjs(current).date(parseInt(date, 10)); 77 | 78 | if (!date.trim()) { 79 | return ''; 80 | } 81 | if (!customDayText) { 82 | return ''; 83 | } 84 | 85 | return customDayText(currentDate); 86 | }; 87 | 88 | public isIncludeDay = (date: string, dates?: dayjs.Dayjs[]): boolean => { 89 | const { current } = this.props; 90 | if (dates === undefined) { 91 | return false; 92 | } 93 | return dates.some(v => isDayEqual(dayjs(current).date(parseInt(date, 10)), v)); 94 | }; 95 | 96 | public handleClick = (date: string) => { 97 | const { current, disableDay } = this.props; 98 | const currentDate = dayjs(current).date(parseInt(date, 10)); 99 | if (!(disableDay && disableDay(currentDate))) { 100 | ifExistCall(this.props.onClick, date); 101 | } 102 | }; 103 | 104 | public handleMouseOver = (date: string) => { 105 | const { onMouseOver, current } = this.props; 106 | ifExistCall(onMouseOver, dayjs(current).date(parseInt(date, 10))); 107 | }; 108 | 109 | public render() { 110 | const { current, locale } = this.props; 111 | 112 | const dayMatrix = getDayMatrix(dayjs(current).year(), dayjs(current).month()); 113 | const weekdays = getWeekDays(locale); 114 | 115 | return ( 116 | ( 120 | 128 | )} 129 | /> 130 | ); 131 | } 132 | } 133 | 134 | export default DayView; 135 | -------------------------------------------------------------------------------- /src/components/Picker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as CX from 'classnames'; 3 | import { IDatePicker } from '../common/@types'; 4 | import { getDivPosition, getDomHeight } from '../utils/DOMUtil'; 5 | import Backdrop from './Backdrop'; 6 | 7 | export interface PickerAction { 8 | show: () => void; 9 | hide: () => void; 10 | } 11 | 12 | export interface PickerRenderProps { 13 | actions: PickerAction; 14 | } 15 | 16 | export interface PickerProps { 17 | /** DatePicker portal version */ 18 | portal?: boolean; 19 | /** DatePicker show direction (0 = TOP , 1 = BOTTOM) */ 20 | direction?: IDatePicker.PickerDirection; 21 | } 22 | 23 | export interface Props { 24 | readOnly?: boolean; 25 | disabled?: boolean; 26 | className?: string; 27 | renderTrigger: (props: PickerRenderProps) => JSX.Element; 28 | renderContents: (props: PickerRenderProps) => JSX.Element; 29 | } 30 | 31 | export interface State { 32 | show: boolean; 33 | position: IDatePicker.Position; 34 | } 35 | class Picker extends React.Component { 36 | public state = { 37 | show: false, 38 | position: { 39 | left: '', 40 | top: '', 41 | }, 42 | }; 43 | 44 | private triggerRef: React.RefObject; 45 | private contentsRef: React.RefObject; 46 | 47 | constructor(props: Props) { 48 | super(props); 49 | this.triggerRef = React.createRef(); 50 | this.contentsRef = React.createRef(); 51 | } 52 | 53 | public showContents = () => { 54 | const { portal, disabled, readOnly } = this.props; 55 | if (disabled || readOnly) return; 56 | 57 | this.setState( 58 | { 59 | show: true, 60 | }, 61 | () => { 62 | if (!portal) { 63 | this.setPosition(); 64 | } 65 | } 66 | ); 67 | }; 68 | 69 | public hideContents = () => { 70 | this.setState({ 71 | show: false, 72 | }); 73 | }; 74 | 75 | public setPosition = () => { 76 | const { direction } = this.props; 77 | this.setState({ 78 | position: getDivPosition( 79 | this.triggerRef.current, 80 | direction, 81 | getDomHeight(this.contentsRef.current) 82 | ), 83 | }); 84 | }; 85 | 86 | public render() { 87 | const { portal, className, renderTrigger, renderContents } = this.props; 88 | const { show, position } = this.state; 89 | const actions = { 90 | show: this.showContents, 91 | hide: this.hideContents, 92 | }; 93 | 94 | return ( 95 |
96 |
97 | {renderTrigger({ actions })} 98 |
99 | {show && ( 100 |
107 | {renderContents({ actions })} 108 |
109 | )} 110 | 111 |
112 | ); 113 | } 114 | } 115 | 116 | export default Picker; 117 | -------------------------------------------------------------------------------- /src/components/PickerInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as classNames from 'classnames'; 3 | import SVGIcon from './SVGIcon'; 4 | 5 | export interface Props { 6 | /** Picker Input value */ 7 | value?: string; 8 | /** Picker Input Readonly */ 9 | readOnly?: boolean; 10 | /** Picker Input Disabled */ 11 | disabled?: boolean; 12 | /** Picker Input clear icon */ 13 | clear?: boolean; 14 | /** Picker Input show icon */ 15 | icon?: JSX.Element; 16 | /** Picker Input onChange Event */ 17 | onChange: (e: React.FormEvent) => void; 18 | /** Picker Input onBlur Event */ 19 | onBlur?: (e: React.FormEvent) => void; 20 | /** Picker Input onClick Event */ 21 | onClick?: () => void; 22 | /** Picker Input onClear Event */ 23 | onClear?: () => void; 24 | /** Picker Input Auto Focus */ 25 | autoFocus?: boolean; 26 | /** Picker Input ClassName */ 27 | className?: string; 28 | /** Picker Input Placeholder */ 29 | placeholder?: string; 30 | } 31 | 32 | class PickerInput extends React.Component { 33 | public inputRef: React.RefObject; 34 | 35 | constructor(props: Props) { 36 | super(props); 37 | this.inputRef = React.createRef(); 38 | } 39 | 40 | public componentDidMount() { 41 | const { current } = this.inputRef; 42 | const { autoFocus } = this.props; 43 | 44 | if (current && autoFocus) { 45 | current.focus(); 46 | } 47 | } 48 | 49 | public handleClear = (e: React.MouseEvent) => { 50 | const { onClear } = this.props; 51 | if (onClear) onClear(); 52 | e.stopPropagation(); 53 | }; 54 | 55 | public renderInput = () => { 56 | const { 57 | readOnly = false, 58 | disabled = false, 59 | value = '', 60 | icon, 61 | onChange, 62 | onClick, 63 | onBlur, 64 | placeholder, 65 | } = this.props; 66 | 67 | return ( 68 | 83 | ); 84 | }; 85 | 86 | public renderClear = () => { 87 | return ( 88 | 89 | 90 | 91 | ); 92 | }; 93 | 94 | public render() { 95 | const { clear, icon, className } = this.props; 96 | return ( 97 |
98 | {icon && {icon}} 99 | {this.renderInput()} 100 | {clear && this.renderClear()} 101 |
102 | ); 103 | } 104 | } 105 | 106 | export default PickerInput; 107 | -------------------------------------------------------------------------------- /src/components/RangeDatePicker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as dayjs from 'dayjs'; 3 | import { isDayAfter, isDayBefore, isDayEqual, isDayRange, formatDate } from '../utils/DateUtil'; 4 | import { DatePickerDefaults } from '../common/Constant'; 5 | import Picker, { PickerProps, PickerAction } from './Picker'; 6 | import RangePickerInput, { FieldType, InputProps } from './RangePickerInput'; 7 | import Calendar, { Props as ICalendarProps } from './Calendar'; 8 | import { Merge, Omit } from '../utils/TypeUtil'; 9 | import { ifExistCall } from '../utils/FunctionUtil'; 10 | 11 | interface RangeDatePickerProps { 12 | /** To display input format (Day.js format) */ 13 | dateFormat: string; 14 | /** Initial Calendar base date(if start date not set) */ 15 | initialDate: dayjs.Dayjs; 16 | /** Initial Start Date */ 17 | initialStartDate?: dayjs.Dayjs; 18 | /** Initial End Date */ 19 | initialEndDate?: dayjs.Dayjs; 20 | /** RangeDatePicker change event */ 21 | onChange?: (start?: dayjs.Dayjs, end?: dayjs.Dayjs) => void; 22 | /** start day display this prop(optional) */ 23 | startText: string; 24 | /** end day display this prop(optional) */ 25 | endText: string; 26 | /** calendar wrapping element */ 27 | wrapper?: (calendar: JSX.Element) => JSX.Element; 28 | } 29 | 30 | export interface State { 31 | start?: dayjs.Dayjs; 32 | end?: dayjs.Dayjs; 33 | hoverDate?: dayjs.Dayjs; 34 | startValue: string; 35 | endValue: string; 36 | mode?: FieldType; 37 | } 38 | 39 | type CalendarProps = Merge< 40 | Omit, 41 | { 42 | showMonthCnt?: number; 43 | } 44 | >; 45 | 46 | export type Props = RangeDatePickerProps & CalendarProps & InputProps & PickerProps; 47 | 48 | class RangeDatePicker extends React.Component { 49 | public static defaultProps = { 50 | dateFormat: DatePickerDefaults.dateFormat, 51 | portal: false, 52 | initialDate: dayjs(), 53 | showMonthCnt: 2, 54 | startText: '', 55 | endText: '', 56 | }; 57 | 58 | public constructor(props: Props) { 59 | super(props); 60 | const { dateFormat, initialStartDate, initialEndDate } = props; 61 | const start = initialStartDate; 62 | const end = initialEndDate; 63 | 64 | this.state = { 65 | start, 66 | end, 67 | startValue: formatDate(start, dateFormat), 68 | endValue: formatDate(end, dateFormat), 69 | }; 70 | } 71 | 72 | public handleDateChange = (actions: PickerAction) => (date: dayjs.Dayjs) => { 73 | const { onChange, dateFormat } = this.props; 74 | const { start, end } = this.state; 75 | let startDate: dayjs.Dayjs | undefined; 76 | let endDate: dayjs.Dayjs | undefined; 77 | 78 | startDate = start; 79 | endDate = end; 80 | 81 | if (!start) { 82 | startDate = date; 83 | } else { 84 | if (end) { 85 | startDate = date; 86 | endDate = undefined; 87 | } else { 88 | if (!isDayBefore(date, start)) { 89 | endDate = date; 90 | } else { 91 | startDate = date; 92 | } 93 | } 94 | } 95 | 96 | ifExistCall(onChange, startDate, endDate); 97 | 98 | this.setState( 99 | { 100 | ...this.state, 101 | start: startDate, 102 | end: endDate, 103 | startValue: formatDate(startDate, dateFormat), 104 | endValue: formatDate(endDate, dateFormat), 105 | }, 106 | () => { 107 | if (this.state.start && this.state.end) { 108 | actions.hide(); 109 | } 110 | } 111 | ); 112 | }; 113 | 114 | public handleInputChange = (fieldType: FieldType, value: string) => { 115 | const key = fieldType === FieldType.START ? 'startValue' : 'endValue'; 116 | this.setState({ 117 | ...this.state, 118 | [key]: value, 119 | }); 120 | }; 121 | 122 | public handleMouseOver = (date: dayjs.Dayjs) => { 123 | this.setState({ 124 | ...this.state, 125 | hoverDate: date, 126 | }); 127 | }; 128 | 129 | public handleInputBlur = (fieldType: FieldType, value: string) => { 130 | const { dateFormat } = this.props; 131 | const { start, end } = this.state; 132 | const parsedDate = dayjs(value, dateFormat); 133 | let startDate = start; 134 | let endDate = end; 135 | 136 | if (parsedDate.isValid() && dateFormat.length === value.length) { 137 | if (fieldType === FieldType.END) { 138 | endDate = parsedDate; 139 | } else if (fieldType === FieldType.START) { 140 | startDate = parsedDate; 141 | } 142 | } 143 | 144 | if (startDate && endDate) { 145 | if (isDayBefore(endDate, startDate) || isDayAfter(startDate, endDate)) { 146 | // Swapping Date 147 | let temp: dayjs.Dayjs; 148 | temp = startDate; 149 | startDate = endDate; 150 | endDate = temp; 151 | } 152 | } 153 | 154 | this.setState({ 155 | ...this.state, 156 | start: startDate, 157 | end: endDate, 158 | startValue: formatDate(startDate, dateFormat), 159 | endValue: formatDate(endDate, dateFormat), 160 | }); 161 | }; 162 | 163 | public handleCalendarText = (date: dayjs.Dayjs) => { 164 | const { startText, endText, customDayText } = this.props; 165 | const { start, end } = this.state; 166 | if (isDayEqual(start, date)) return startText; 167 | if (isDayEqual(end, date)) return endText; 168 | ifExistCall(customDayText, date); 169 | return ''; 170 | }; 171 | 172 | public handleCalendarClass = (date: dayjs.Dayjs) => { 173 | const { customDayClass } = this.props; 174 | const { start, end, hoverDate } = this.state; 175 | if (start && !end && hoverDate) { 176 | if (isDayRange(date, start, hoverDate)) { 177 | return 'calendar__day--range'; 178 | } 179 | } 180 | ifExistCall(customDayClass, date); 181 | return ''; 182 | }; 183 | 184 | public handleInputClear = (fieldType: FieldType) => { 185 | if (fieldType === FieldType.START) { 186 | this.setState({ 187 | ...this.state, 188 | start: undefined, 189 | startValue: '', 190 | }); 191 | } else if (fieldType === FieldType.END) { 192 | this.setState({ 193 | ...this.state, 194 | end: undefined, 195 | endValue: '', 196 | }); 197 | } 198 | }; 199 | 200 | public renderRangePickerInput = () => { 201 | const { startPlaceholder, endPlaceholder, readOnly, disabled, clear, onChange } = this.props; 202 | const { startValue, endValue } = this.state; 203 | return ( 204 | 216 | ); 217 | }; 218 | 219 | public renderCalendar = (actions: PickerAction) => { 220 | const { showMonthCnt, initialDate, wrapper } = this.props; 221 | const { start, end } = this.state; 222 | let component: JSX.Element; 223 | 224 | const calendar = ( 225 | 236 | ); 237 | 238 | component = calendar; 239 | 240 | if (wrapper) { 241 | component = wrapper(calendar); 242 | } 243 | 244 | return component; 245 | }; 246 | 247 | public render() { 248 | const { portal, direction, disabled, readOnly } = this.props; 249 | 250 | return ( 251 | this.renderRangePickerInput()} 257 | renderContents={({ actions }) => this.renderCalendar(actions)} 258 | /> 259 | ); 260 | } 261 | } 262 | 263 | export default RangeDatePicker; 264 | -------------------------------------------------------------------------------- /src/components/RangePickerInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import PickerInput, { Props as IPickerInputProps } from './PickerInput'; 3 | import { Merge, Omit } from '../utils/TypeUtil'; 4 | import { ifExistCall } from '../utils/FunctionUtil'; 5 | import SVGIcon from './SVGIcon'; 6 | 7 | export enum FieldType { 8 | START, 9 | END, 10 | } 11 | 12 | interface RangePickerInputProps { 13 | /** start input value */ 14 | startValue?: string; 15 | /** end input value */ 16 | endValue?: string; 17 | /** RangePickerInput change event field type is start or end */ 18 | onChange?: (fieldType: FieldType, value: string) => void; 19 | /** RangePickerInput Blur event field type is start or end */ 20 | onBlur?: (fieldType: FieldType, value: string) => void; 21 | /** RangePickerInput click event field type is start or end */ 22 | onClick?: (fieldTyp: FieldType) => void; 23 | /** RangePickerInput clear event */ 24 | onClear?: (fieldType: FieldType) => void; 25 | } 26 | 27 | export type InputProps = Merge< 28 | Omit, 29 | { 30 | /** start input placeholder */ 31 | startPlaceholder?: string; 32 | /** end input placeholder */ 33 | endPlaceholder?: string; 34 | } 35 | >; 36 | 37 | type Props = RangePickerInputProps & InputProps; 38 | 39 | class RangePickerInput extends React.Component { 40 | public handleChange = (fieldType: FieldType) => (e: React.FormEvent) => 41 | ifExistCall(this.props.onChange, fieldType, e.currentTarget.value); 42 | public handleBlur = (fieldType: FieldType) => (e: React.FormEvent) => 43 | ifExistCall(this.props.onBlur, fieldType, e.currentTarget.value); 44 | public handleClick = (fieldType: FieldType) => () => ifExistCall(this.props.onClick, fieldType); 45 | public handleClear = (fieldType: FieldType) => () => ifExistCall(this.props.onClear, fieldType); 46 | 47 | public renderStartInput = () => { 48 | const { startValue, startPlaceholder } = this.props; 49 | return this.renderPickerInput(FieldType.START, startValue, startPlaceholder); 50 | }; 51 | 52 | public renderEndInput = () => { 53 | const { endValue, endPlaceholder } = this.props; 54 | return this.renderPickerInput(FieldType.END, endValue, endPlaceholder); 55 | }; 56 | 57 | public renderPickerInput = (fieldType: FieldType, value?: string, placeholder?: string) => { 58 | const { readOnly, disabled, clear } = this.props; 59 | return ( 60 | 72 | ); 73 | }; 74 | 75 | public render() { 76 | return ( 77 |
78 | {this.renderStartInput()} 79 | 80 | 81 | 82 | {this.renderEndInput()} 83 |
84 | ); 85 | } 86 | } 87 | 88 | export default RangePickerInput; 89 | -------------------------------------------------------------------------------- /src/components/SVGIcon/IconBase.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IDatePicker } from '../../common/@types'; 3 | 4 | const IconBase: React.FunctionComponent = props => ( 5 | 13 | {props.children} 14 | 15 | ); 16 | 17 | export default IconBase; 18 | -------------------------------------------------------------------------------- /src/components/SVGIcon/Icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IDatePicker } from '../../common/@types'; 3 | import IconBase from './IconBase'; 4 | 5 | const Calendar: React.FunctionComponent = props => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | const Clear: React.FunctionComponent = props => ( 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | const LeftArrow: React.FunctionComponent = props => ( 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | const RightArrow: React.FunctionComponent = props => ( 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | const Time: React.FunctionComponent = props => ( 34 | 35 | 36 | 37 | 38 | ); 39 | 40 | const Up: React.FunctionComponent = props => ( 41 | 42 | 43 | 44 | 45 | ); 46 | 47 | const Down: React.FunctionComponent = props => ( 48 | 49 | 50 | 51 | 52 | ); 53 | export { Calendar, Clear, LeftArrow, RightArrow, Time, Up, Down }; 54 | -------------------------------------------------------------------------------- /src/components/SVGIcon/SVGIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IDatePicker } from '../../common/@types'; 3 | import { Calendar, Clear, Down, LeftArrow, RightArrow, Time, Up} from './Icons'; 4 | 5 | interface Props extends IDatePicker.SVGIconProps { 6 | id: string; 7 | } 8 | 9 | const SVGIcon: React.FunctionComponent = props => { 10 | const iconMap = { 11 | 'calendar': Calendar, 12 | 'clear': Clear, 13 | 'time': Time, 14 | 'left-arrow': LeftArrow, 15 | 'right-arrow': RightArrow, 16 | 'down': Down, 17 | 'up': Up, 18 | }; 19 | 20 | const Icon = iconMap[props.id]; 21 | 22 | return ; 23 | }; 24 | 25 | SVGIcon.defaultProps = { 26 | size: '16', 27 | color: 'currentColor', 28 | }; 29 | 30 | export default SVGIcon; 31 | -------------------------------------------------------------------------------- /src/components/SVGIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import SVGIcon from './SVGIcon'; 2 | 3 | export default SVGIcon; 4 | -------------------------------------------------------------------------------- /src/components/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ifExistCall } from '../utils/FunctionUtil'; 3 | 4 | interface Props { 5 | className?: string; 6 | text?: string; 7 | subText?: string; 8 | onClick?: (text: string) => void; 9 | onMouseOver?: (text: string) => void; 10 | } 11 | 12 | const Cell: React.FunctionComponent = ({ className, text, subText, onClick, onMouseOver }) => { 13 | return ( 14 | ifExistCall(onClick, text)} 16 | onMouseOver={() => ifExistCall(onMouseOver, text)} 17 | className={className} 18 | > 19 |
{text}
20 | {subText && {subText}} 21 | 22 | ); 23 | }; 24 | 25 | export default Cell; 26 | -------------------------------------------------------------------------------- /src/components/TableMatrixView.tsx: -------------------------------------------------------------------------------- 1 | import * as classNames from 'classnames'; 2 | import * as React from 'react'; 3 | 4 | interface Props { 5 | matrix: string[][]; 6 | headers?: string[]; 7 | className?: string; 8 | cell: (value: string, key: number) => JSX.Element; 9 | } 10 | 11 | const TableMatrixView: React.FunctionComponent = ({ className, matrix, cell, headers }) => { 12 | return ( 13 | 14 | {headers && ( 15 | 16 | 17 | {headers.map((v, i) => ( 18 | 19 | ))} 20 | 21 | 22 | )} 23 | 24 | {matrix.map((row, i) => ( 25 | {row.map((v, j) => cell(v, i * matrix[i].length + j))} 26 | ))} 27 | 28 |
{v}
29 | ); 30 | }; 31 | 32 | export default TableMatrixView; 33 | -------------------------------------------------------------------------------- /src/components/TimeContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import TimeInput from './TimeInput'; 3 | import { ifExistCall } from '../utils/FunctionUtil'; 4 | 5 | interface Props { 6 | /** hour to display */ 7 | hour?: number; 8 | /** minute to display */ 9 | minute?: number; 10 | /** hour, minute, type change event */ 11 | onChange?: (hour: number, minute: number) => void; 12 | /** hour, minute blur event */ 13 | onBlur?: (hour: number, minute: number) => void; 14 | } 15 | 16 | interface State { 17 | hour: number; 18 | minute: number; 19 | } 20 | 21 | class TimeContainer extends React.Component { 22 | public state = { 23 | hour: this.props.hour || 0, 24 | minute: this.props.minute || 0, 25 | }; 26 | 27 | public handleChange = (item: string) => (e: React.FormEvent) => { 28 | const min = 0; 29 | const max = item === 'hour' ? 23 : 59; 30 | let value = parseInt(e.currentTarget.value, 10); 31 | 32 | if (isNaN(value)) { 33 | value = 0; 34 | } 35 | 36 | if (max < value) { 37 | value = max; 38 | } 39 | 40 | if (min > value) { 41 | value = min; 42 | } 43 | 44 | this.setState( 45 | { 46 | ...this.state, 47 | [item]: value, 48 | }, 49 | () => this.invokeOnChange() 50 | ); 51 | }; 52 | 53 | public handleUp = (item: string) => () => { 54 | const max = item === 'hour' ? 23 : 59; 55 | 56 | const value = this.state[item]; 57 | 58 | this.setState( 59 | { 60 | ...this.state, 61 | [item]: Math.min(value + 1, max), 62 | }, 63 | () => this.invokeOnChange() 64 | ); 65 | }; 66 | 67 | public handleDown = (item: string) => () => { 68 | const min = 0; 69 | const value = this.state[item]; 70 | this.setState( 71 | { 72 | ...this.state, 73 | [item]: Math.max(value - 1, min), 74 | }, 75 | () => this.invokeOnChange() 76 | ); 77 | }; 78 | 79 | public handleBlur = () => { 80 | const { onBlur } = this.props; 81 | const { hour, minute } = this.state; 82 | ifExistCall(onBlur, hour, minute); 83 | }; 84 | 85 | public invokeOnChange = () => { 86 | const { onChange } = this.props; 87 | const { hour, minute } = this.state; 88 | ifExistCall(onChange, hour, minute); 89 | }; 90 | 91 | public render() { 92 | const { hour, minute } = this.state; 93 | return ( 94 |
95 | 102 |
:
103 | 110 |
111 | ); 112 | } 113 | } 114 | 115 | export default TimeContainer; 116 | -------------------------------------------------------------------------------- /src/components/TimeInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SVGIcon from './SVGIcon'; 3 | 4 | export interface Props { 5 | /** TimeInput onUp event */ 6 | onUp: () => void; 7 | /** TimeInput onDown event */ 8 | onDown: () => void; 9 | /** TimeInput onChange event */ 10 | onChange: (e: React.FormEvent) => void; 11 | /** TimeInput onBlur event */ 12 | onBlur: (e: React.FormEvent) => void; 13 | /** TimeInput value */ 14 | value: number; 15 | } 16 | 17 | const TimeInput: React.FunctionComponent = ({ onUp, onDown, onChange, onBlur, value }) => { 18 | return ( 19 |
20 |
21 | 24 |
25 |
26 | 27 |
28 |
29 | 32 |
33 |
34 | ); 35 | }; 36 | 37 | TimeInput.defaultProps = { 38 | value: 0, 39 | }; 40 | 41 | export default TimeInput; 42 | -------------------------------------------------------------------------------- /src/components/TodayPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as classNames from 'classnames'; 3 | 4 | interface IProps { 5 | /** panel display today string */ 6 | today: string; 7 | /** today panel click event */ 8 | onClick?: () => void; 9 | /** today panel show or hide */ 10 | show?: boolean; 11 | } 12 | 13 | const TodayPanel: React.FunctionComponent = ({ today, show, onClick }) => ( 14 |
15 |

{today}

16 |
17 | ); 18 | 19 | export default TodayPanel; 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Calendar from './components/Calendar'; 2 | import DatePicker from './components/DatePicker'; 3 | import RangeDatePicker from './components/RangeDatePicker'; 4 | 5 | export { Calendar, DatePicker, RangeDatePicker }; 6 | -------------------------------------------------------------------------------- /src/utils/ArrayUtil.ts: -------------------------------------------------------------------------------- 1 | export const chunk = (arr: any[], n: number) => { 2 | const result = []; 3 | let i = 0; 4 | while (i < arr.length / n) { 5 | result.push(arr.slice(i * n, i * n + n)); 6 | i += 1; 7 | } 8 | 9 | return result; 10 | }; 11 | 12 | export const range = (n1: number, n2?: number) => { 13 | const result = []; 14 | let first = !n2 ? 0 : n1; 15 | let last = n2; 16 | 17 | if (!last) { 18 | last = n1; 19 | } 20 | 21 | while (first < last) { 22 | result.push(first); 23 | first += 1; 24 | } 25 | return result; 26 | }; 27 | 28 | export const repeat = (el: any, n: number) => { 29 | return range(n).map(() => el); 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/DOMUtil.ts: -------------------------------------------------------------------------------- 1 | import { IDatePicker } from '../common/@types'; 2 | const convertPx = (value: number) => `${value}px`; 3 | /** 4 | * Getting Div position as far as distance 5 | * @param node 6 | * @param direction 7 | * @param distance 8 | */ 9 | export const getDivPosition = ( 10 | node: HTMLDivElement | null, 11 | direction: IDatePicker.PickerDirection = IDatePicker.PickerDirection.BOTTOM, 12 | height: number, 13 | distance: number = 5 14 | ): IDatePicker.Position => { 15 | if (!node) return { left: '', top: '', bottom: '' }; 16 | 17 | let top = 0; 18 | let left = 0; 19 | 20 | switch (direction) { 21 | case IDatePicker.PickerDirection.BOTTOM: 22 | top = node.offsetTop + node.offsetHeight + distance; 23 | left = node.offsetLeft; 24 | break; 25 | case IDatePicker.PickerDirection.TOP: 26 | top = node.offsetTop - height - distance; 27 | left = node.offsetLeft; 28 | break; 29 | } 30 | 31 | return { 32 | top: convertPx(top), 33 | left: convertPx(left), 34 | }; 35 | }; 36 | 37 | export const getDomHeight = (node: HTMLDivElement | null): number => { 38 | return node ? node.clientHeight : 0; 39 | }; 40 | -------------------------------------------------------------------------------- /src/utils/DateUtil.ts: -------------------------------------------------------------------------------- 1 | import { chunk, repeat, range } from './ArrayUtil'; 2 | import { IDatePicker } from '../common/@types'; 3 | import * as dayjs from 'dayjs'; 4 | import { getMonthShort } from './LocaleUtil'; 5 | 6 | export const getDayMatrix = (year: number, month: number): string[][] => { 7 | const date = dayjs() 8 | .year(year) 9 | .month(month); 10 | 11 | const startOfMonth = date.startOf('month').date(); 12 | const endOfMonth = date.endOf('month').date(); 13 | 14 | const startDay = date.startOf('month').day(); 15 | const remain = (startDay + endOfMonth) % 7; 16 | 17 | return chunk( 18 | [ 19 | ...repeat(' ', startDay), 20 | ...range(startOfMonth, endOfMonth + 1).map(v => `${v}`), 21 | ...(7 - remain === 7 ? [] : repeat(' ', 7 - remain)), 22 | ], 23 | 7 24 | ); 25 | }; 26 | 27 | export const getMonthMatrix = (locale: IDatePicker.Locale) => { 28 | return chunk(getMonthShort(locale), 3); 29 | }; 30 | 31 | export const getYearMatrix = (year: number): string[][] => { 32 | return chunk(range(year - 4, year + 5).map(v => `${v}`), 3); 33 | }; 34 | 35 | export const isDayEqual = (day1?: dayjs.Dayjs, day2?: dayjs.Dayjs) => { 36 | if (!day1 || !day2) return false; 37 | return dayjs(day1).isSame(day2, 'date'); 38 | }; 39 | 40 | export const isDayAfter = (day1: dayjs.Dayjs, day2: dayjs.Dayjs) => { 41 | return dayjs(day1).isAfter(day2, 'date'); 42 | }; 43 | 44 | export const isDayBefore = (day1: dayjs.Dayjs, day2: dayjs.Dayjs) => { 45 | return dayjs(day1).isBefore(day2, 'date'); 46 | }; 47 | 48 | export const isDayRange = (date: dayjs.Dayjs, start?: dayjs.Dayjs, end?: dayjs.Dayjs) => { 49 | if (!start || !end) return false; 50 | 51 | return isDayAfter(date, start) && isDayBefore(date, end); 52 | }; 53 | 54 | export const formatDate = (date: dayjs.Dayjs | undefined, format: string) => { 55 | if (date === undefined) return ''; 56 | return dayjs(date).format(format); 57 | }; 58 | -------------------------------------------------------------------------------- /src/utils/FunctionUtil.ts: -------------------------------------------------------------------------------- 1 | export const ifExistCall = (func?: (...args: any[]) => void, ...args: any[]) => 2 | func && func(...args); 3 | -------------------------------------------------------------------------------- /src/utils/LocaleUtil.ts: -------------------------------------------------------------------------------- 1 | import * as dayjs from 'dayjs'; 2 | import { IDatePicker } from '../common/@types'; 3 | import { range } from '../utils/ArrayUtil'; 4 | import * as localeData from 'dayjs/plugin/localeData'; 5 | import * as localizedFormat from 'dayjs/plugin/localizedFormat'; 6 | import * as weekday from 'dayjs/plugin/weekday'; 7 | 8 | dayjs.extend(localeData); 9 | dayjs.extend(localizedFormat); 10 | dayjs.extend(weekday); 11 | 12 | export const getMonthShort = (locale: IDatePicker.Locale) => { 13 | dayjs.locale(locale); 14 | return range(0, 12).map(v => 15 | dayjs() 16 | .localeData() 17 | .monthsShort(dayjs().month(v)) 18 | ); 19 | }; 20 | 21 | export const getWeekDays = (locale: IDatePicker.Locale) => { 22 | dayjs.locale(locale); 23 | return range(7).map(v => 24 | dayjs() 25 | .localeData() 26 | .weekdaysShort(dayjs().weekday(v)) 27 | ); 28 | }; 29 | 30 | export const getToday = (locale: IDatePicker.Locale) => { 31 | return dayjs() 32 | .locale(locale) 33 | .format('LL'); 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/StringUtil.ts: -------------------------------------------------------------------------------- 1 | export const lpad = (val: string, length: number, char: string = '0') => 2 | val.length < length ? char.repeat(length - val.length) + val : val; 3 | -------------------------------------------------------------------------------- /src/utils/TypeUtil.ts: -------------------------------------------------------------------------------- 1 | type Omit = Pick>; 2 | type Merge = Omit> & N; 3 | 4 | export { Omit, Merge }; 5 | -------------------------------------------------------------------------------- /stories/Calendar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import * as dayjs from 'dayjs'; 4 | import CalendarSelectedController from '../examples/CalendarSelectedController'; 5 | import { number } from '@storybook/addon-knobs'; 6 | import './css/custom.css'; 7 | import 'dayjs/locale/ko'; 8 | import 'dayjs/locale/zh-cn'; 9 | import 'dayjs/locale/ja'; 10 | 11 | storiesOf('Calendar', module) 12 | .add('default', () => { 13 | return ; 14 | }) 15 | .add('i18n internalizeation', () => { 16 | return ( 17 |
18 |
19 |

Korea

20 | 21 |
22 |
23 |

Japen

24 | 25 |
26 |
27 |

China

28 | 29 |
30 |
31 | ); 32 | }) 33 | .add('custom locale object', () => { 34 | return ( 35 | 43 | ); 44 | }) 45 | .add('todayPanel', () => { 46 | return ; 47 | }) 48 | .add('showMonthCnt', () => { 49 | const showMontCnt = number('showMonthCnt', 2); 50 | return ; 51 | }) 52 | .add('disableDay', () => { 53 | const disableDay = (date: dayjs.Dayjs) => { 54 | return dayjs(date).date() < 7; 55 | }; 56 | return ; 57 | }) 58 | .add('selected & onChange', () => { 59 | return ; 60 | }) 61 | .add('multiple select', () => { 62 | return ; 63 | }) 64 | .add('custom day class', () => { 65 | const customDayClass = (date: dayjs.Dayjs) => { 66 | // for test (year, month remove) 67 | const classMap = { 68 | '01': 'custom-class', 69 | '02': 'day-test1', 70 | }; 71 | 72 | return classMap[dayjs(date).format('DD')]; 73 | }; 74 | return ( 75 |
76 | 77 | custom-class, day-test class example 78 |
79 | ); 80 | }) 81 | .add('custom day text', () => { 82 | const customDayText = (date: dayjs.Dayjs) => { 83 | // for test (year, month remove) 84 | const classMap = { 85 | '01': '첫째날', 86 | '02': '둘째날', 87 | }; 88 | 89 | return classMap[dayjs(date).format('DD')]; 90 | }; 91 | return ; 92 | }); 93 | -------------------------------------------------------------------------------- /stories/DatePicker.stories.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import * as dayjs from 'dayjs'; 4 | import { action } from '@storybook/addon-actions'; 5 | import { text } from '@storybook/addon-knobs'; 6 | import DatePicker from '../src/components/DatePicker'; 7 | import LayoutDecorator from './decorator/LayoutDecorator'; 8 | 9 | const defaultProps = { 10 | onChange: action('onChange'), 11 | locale: 'en', 12 | }; 13 | storiesOf('DatePicker', module) 14 | .addDecorator(LayoutDecorator) 15 | .add('default', () => { 16 | return ; 17 | }) 18 | .add('initialDate', () => { 19 | return ; 20 | }) 21 | .add('portal version', () => ) 22 | .add('includeTime', () => { 23 | return ; 24 | }) 25 | .add('showTimeOnly', () => { 26 | return ; 27 | }) 28 | .add('dateFormat', () => { 29 | return ; 30 | }) 31 | .add('showMonthCnt', () => { 32 | return ; 33 | }) 34 | .add('onTop', () => { 35 | return ( 36 |
37 | 38 |
39 | ); 40 | }) 41 | .add('placeholder', () => { 42 | return ; 43 | }); 44 | 45 | storiesOf('DatePicker - Input Props', module) 46 | .addDecorator(LayoutDecorator) 47 | .add('showDefaultIcon', () => { 48 | return ; 49 | }) 50 | .add('readOnly', () => { 51 | return ; 52 | }) 53 | .add('disabled', () => { 54 | return ; 55 | }) 56 | .add('clear', () => { 57 | return ; 58 | }); 59 | -------------------------------------------------------------------------------- /stories/PickerInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import PickerInput from '../src/components/PickerInput'; 5 | 6 | if (process.env.NODE_ENV === 'development') { 7 | storiesOf('PickerInput', module) 8 | .add('default', () => ) 9 | .add('readOnly', () => ) 10 | .add('disabled', () => ) 11 | .add('autoFocus', () => ) 12 | .add('clear', () => ); 13 | } 14 | -------------------------------------------------------------------------------- /stories/RangeDatePicker.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as dayjs from 'dayjs'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { text } from '@storybook/addon-knobs'; 5 | import RangeDatePicker from '../src/components/RangeDatePicker'; 6 | import LayoutDecorator from './decorator/LayoutDecorator'; 7 | 8 | storiesOf('RangeDatePicker', module) 9 | .addDecorator(LayoutDecorator) 10 | .add('default', () => ) 11 | .add('initial Start & End Date', () => { 12 | return ( 13 | 14 | ); 15 | }) 16 | .add('startText & endText', () => ( 17 | 18 | )) 19 | .add('portal version', () => ( 20 | 25 | )) 26 | .add('onTop', () => { 27 | return ( 28 |
29 | 34 |
35 | ); 36 | }) 37 | .add('show 3 month', () => { 38 | return ; 39 | }) 40 | .add('dateFormat', () => { 41 | return ; 42 | }) 43 | .add('wrapping calendar', () => { 44 | const wrapper = (calendar: JSX.Element) => ( 45 |
46 |

47 | Please select a reservation date 48 |

49 | {calendar} 50 |
51 | ); 52 | 53 | return ( 54 | 61 | ); 62 | }); 63 | 64 | storiesOf('RangeDatePicker - Input Props', module) 65 | .addDecorator(LayoutDecorator) 66 | .add('readOnly', () => ) 67 | .add('disabled', () => ) 68 | .add('clear', () => ) 69 | .add('placeholder', () => ( 70 | 74 | )); 75 | -------------------------------------------------------------------------------- /stories/TimeContainer.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import TimeContainer from '../src/components/TimeContainer'; 5 | 6 | storiesOf('TimeContainer', module).add( 7 | 'default', 8 | () => , 9 | { 10 | jest: ['TimeContainer'], 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /stories/TimeInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import TimeInput from '../src/components/TimeInput'; 5 | 6 | storiesOf('TimeInput', module) 7 | // .addParameters({ jest: ['TimeInput'] }) 8 | .add( 9 | 'default(min, max)', 10 | () => ( 11 | 18 | ), 19 | { 20 | jest: ['TimeInput'], 21 | } 22 | ); 23 | -------------------------------------------------------------------------------- /stories/css/custom.css: -------------------------------------------------------------------------------- 1 | .custom-class { 2 | background: red; 3 | color: white; 4 | } 5 | 6 | .day-test1 { 7 | background: blue; 8 | color: white; 9 | } 10 | 11 | .panel { 12 | display: inline-flex; 13 | flex-direction: column; 14 | margin: 0 10px; 15 | } -------------------------------------------------------------------------------- /stories/decorator/LayoutDecorator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const LayoutDecorator = (storyFn: any) => ( 4 |
9 | {storyFn()} 10 |
11 | ); 12 | 13 | export default LayoutDecorator; 14 | -------------------------------------------------------------------------------- /test-preprocessor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Code Preprocessing 3 | * Typescript code transplie to javascript 4 | */ 5 | const tsc = require('typescript'); 6 | const tsConfig = require('./tsconfig.json'); 7 | 8 | module.exports = { 9 | process(src, path) { 10 | if (path.endsWith('.ts') || path.endsWith('.tsx') || path.endsWith('.js')) { 11 | return tsc.transpile(src, tsConfig.compilerOptions, path, []); 12 | } 13 | return src; 14 | }, 15 | }; -------------------------------------------------------------------------------- /test-setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React 16 Adapter for Enzyme 3 | */ 4 | const enzyme = require('enzyme'); 5 | const Adapter = require('enzyme-adapter-react-16'); 6 | 7 | enzyme.configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /test-shim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get rids of the missing requestAnimatinoFrame polyfill warning 3 | */ 4 | global.requestAnimationFrame = function(callback) { 5 | setTimeout(callback, 0); 6 | }; 7 | -------------------------------------------------------------------------------- /test/ArrayUtil.test.ts: -------------------------------------------------------------------------------- 1 | import { range, chunk, repeat } from '../src/utils/ArrayUtil'; 2 | describe('ArrayUtil', () => { 3 | it('should range negative number return empty array', () => { 4 | expect(range(-1)).toEqual([]); 5 | }); 6 | it('should range return number array', () => { 7 | expect(range(3)).toEqual([0, 1, 2]); 8 | expect(range(3, 5)).toEqual([3, 4]); 9 | }); 10 | 11 | it('should chunk array return matrix array', () => { 12 | expect(chunk([1, 2, 3, 4, 5, 6, 7, 8, 9], 3)).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); 13 | expect(chunk([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3)).toEqual([ 14 | [1, 2, 3], 15 | [4, 5, 6], 16 | [7, 8, 9], 17 | [10], 18 | ]); 19 | }); 20 | 21 | it('should repeat return character fill array', () => { 22 | expect(repeat(' ', 5)).toEqual([' ', ' ', ' ', ' ', ' ']); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/Calendar.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow, mount, ShallowWrapper, ReactWrapper } from 'enzyme'; 2 | import * as dayjs from 'dayjs'; 3 | import * as React from 'react'; 4 | import Calendar, { Props, State } from '../src/components/Calendar'; 5 | import CalendarContainer from '../src/components/CalendarContainer'; 6 | 7 | describe('', () => { 8 | // 20180501 9 | let shallowComponent: ShallowWrapper>; 10 | let mountComponent: ReactWrapper; 11 | const defaultProps = { 12 | base: dayjs(new Date(2018, 4, 1)), 13 | }; 14 | 15 | it('redners with no props', () => { 16 | shallowComponent = shallow(); 17 | 18 | expect(shallowComponent).toBeTruthy(); 19 | expect(shallowComponent).toMatchSnapshot(); 20 | }); 21 | 22 | it('props showMonthCnt correctly', () => { 23 | shallowComponent = shallow(); 24 | 25 | expect(shallowComponent.find('.calendar__item')).toHaveLength(3); 26 | // first calendar only previcon true 27 | expect( 28 | shallowComponent 29 | .find(CalendarContainer) 30 | .first() 31 | .props().prevIcon 32 | ).toBeTruthy(); 33 | // last calendar only nextIcon true 34 | expect( 35 | shallowComponent 36 | .find(CalendarContainer) 37 | .last() 38 | .props().nextIcon 39 | ).toBeTruthy(); 40 | // another calendar both hide 41 | expect( 42 | shallowComponent 43 | .find(CalendarContainer) 44 | .at(1) 45 | .props().nextIcon 46 | ).toBeFalsy(); 47 | expect( 48 | shallowComponent 49 | .find(CalendarContainer) 50 | .at(1) 51 | .props().prevIcon 52 | ).toBeFalsy(); 53 | }); 54 | 55 | it('should setBase test', () => { 56 | mountComponent = mount(); 57 | // change view mode to month 58 | mountComponent.find('.calendar__head--title').simulate('click'); 59 | 60 | mountComponent 61 | .find('td') 62 | .at(1) 63 | .simulate('click'); 64 | 65 | expect(dayjs(mountComponent.state().base).format('MM')).toEqual('02'); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/CalendarBody.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount, shallow, ReactWrapper, ShallowWrapper } from 'enzyme'; 2 | import * as sinon from 'sinon'; 3 | import * as dayjs from 'dayjs'; 4 | import * as React from 'react'; 5 | import DayView from '../src/components/DayView'; 6 | import CalendarBody from '../src/components/CalendarBody'; 7 | import { IDatePicker } from '../src/common/@types'; 8 | 9 | describe('', () => { 10 | const defaultProps = { 11 | current: dayjs(new Date(2018, 11, 1)), 12 | onClick: sinon.spy(), 13 | }; 14 | let shallowComponent: ShallowWrapper; 15 | let mountComponent: ReactWrapper; 16 | 17 | it('should render correctly', () => { 18 | shallowComponent = shallow(); 19 | expect(shallowComponent).toBeTruthy(); 20 | expect(shallowComponent).toMatchSnapshot(); 21 | }); 22 | 23 | describe('prop: viewMode', () => { 24 | it('should ViewMode.DAY correctly', () => { 25 | shallowComponent = shallow( 26 | 27 | ); 28 | expect(shallowComponent.find(DayView)).toHaveLength(1); 29 | }); 30 | 31 | it('should ViewMode.MONTH correctly', () => { 32 | mountComponent = mount( 33 | 34 | ); 35 | expect(mountComponent.find('td.calendar__month')).toHaveLength(12); 36 | }); 37 | 38 | it('should ViewMode.YEAR correctly', () => { 39 | mountComponent = mount( 40 | 41 | ); 42 | expect(mountComponent.find('td.calendar__year')).toHaveLength(9); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/CalendarContainer.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount, shallow, ShallowWrapper, ReactWrapper } from 'enzyme'; 2 | 3 | import * as dayjs from 'dayjs'; 4 | import * as React from 'react'; 5 | import * as sinon from 'sinon'; 6 | 7 | import 'dayjs/locale/en'; 8 | import 'dayjs/locale/ko'; 9 | 10 | import CalendarContainer, { Props, State } from '../src/components/CalendarContainer'; 11 | import { IDatePicker } from '../src/common/@types'; 12 | 13 | describe('', () => { 14 | const current = dayjs(new Date(2018, 11, 5)); 15 | let base = current; 16 | const setBase = (val: dayjs.Dayjs) => { 17 | base = val; 18 | }; 19 | const defaultProps = { 20 | base, 21 | setBase, 22 | current, 23 | }; 24 | 25 | let shallowComponent: ShallowWrapper>; 26 | let mountComponent: ReactWrapper; 27 | 28 | describe('prop: show', () => { 29 | beforeEach(() => { 30 | shallowComponent = shallow(); 31 | }); 32 | 33 | it('should render correctly', () => { 34 | expect(shallowComponent).toBeTruthy(); 35 | expect(shallowComponent).toMatchSnapshot(); 36 | }); 37 | 38 | it('props show correctly', () => { 39 | expect(shallowComponent.hasClass('calendar--show')).toBeTruthy(); 40 | }); 41 | }); 42 | 43 | describe('prop: locale', () => { 44 | let en: ReactWrapper; 45 | let ko: ReactWrapper; 46 | 47 | beforeEach(() => { 48 | en = mount(); 49 | ko = mount(); 50 | }); 51 | 52 | it('should locale prop correctly', () => { 53 | expect(ko).toMatchSnapshot(); 54 | expect( 55 | en 56 | .find('th') 57 | .first() 58 | .text() 59 | ).toEqual('Sun'); 60 | expect( 61 | ko 62 | .find('th') 63 | .first() 64 | .text() 65 | ).toEqual('일'); 66 | }); 67 | }); 68 | 69 | describe('handle base test(onPrev, onNext)', () => { 70 | beforeEach(() => { 71 | mountComponent = mount( 72 | 73 | ); 74 | }); 75 | 76 | it('should onPrev correctly', () => { 77 | mountComponent.setState({ 78 | viewMode: IDatePicker.ViewMode.DAY, 79 | }); 80 | mountComponent.find('.calendar__head--prev > button').simulate('click'); 81 | expect(dayjs(base).format('MM')).toEqual('11'); 82 | 83 | mountComponent.setState({ 84 | viewMode: IDatePicker.ViewMode.MONTH, 85 | }); 86 | mountComponent.find('.calendar__head--prev > button').simulate('click'); 87 | expect(dayjs(base).format('YYYY')).toEqual('2017'); 88 | 89 | mountComponent.setState({ 90 | viewMode: IDatePicker.ViewMode.YEAR, 91 | }); 92 | mountComponent.find('.calendar__head--prev > button').simulate('click'); 93 | expect(dayjs(base).format('YYYY')).toEqual('2008'); 94 | }); 95 | }); 96 | 97 | describe('handle today test', () => { 98 | it('should handle today correctly', () => { 99 | mountComponent = mount(); 100 | mountComponent.setState({ 101 | viewMode: IDatePicker.ViewMode.DAY, 102 | }); 103 | mountComponent.find('.calendar__head--prev > button').simulate('click'); 104 | mountComponent.find('.calendar__head--prev > button').simulate('click'); 105 | mountComponent.find('.calendar__panel--today h2').simulate('click'); 106 | expect(dayjs(base).format('YYYYMMDD')).toEqual(dayjs().format('YYYYMMDD')); 107 | }); 108 | }); 109 | 110 | describe('handle title click test', () => { 111 | it('should viewMode change correctly', () => { 112 | mountComponent = mount(); 113 | // once click expect month 114 | mountComponent.find('.calendar__head--title').simulate('click'); 115 | expect(mountComponent.state().viewMode).toEqual(IDatePicker.ViewMode.MONTH); 116 | 117 | // twice click expect year 118 | mountComponent.find('.calendar__head--title').simulate('click'); 119 | expect(mountComponent.state().viewMode).toEqual(IDatePicker.ViewMode.YEAR); 120 | }); 121 | 122 | it('should showMontCnt > 1 viewMode noChange', () => { 123 | mountComponent = mount(); 124 | // once click expect day(showMontCnt > 1) 125 | mountComponent.find('.calendar__head--title').simulate('click'); 126 | expect(mountComponent.state().viewMode).toEqual(IDatePicker.ViewMode.DAY); 127 | }); 128 | }); 129 | 130 | describe('handle change test', () => { 131 | it('should showMontCnt > 1 onChange call', () => { 132 | const onChange = sinon.spy(); 133 | mountComponent = mount( 134 | 140 | ); 141 | 142 | // empty date click 143 | mountComponent 144 | .find('td') 145 | .at(1) 146 | .simulate('click'); 147 | 148 | expect(onChange).toHaveProperty('callCount', 0); 149 | 150 | mountComponent 151 | .find('td') 152 | .at(6) 153 | .simulate('click'); 154 | 155 | expect(onChange).toHaveProperty('callCount', 1); 156 | }); 157 | 158 | describe('Mode Specific test', () => { 159 | let onChange: sinon.SinonSpy; 160 | beforeEach(() => { 161 | onChange = sinon.spy(); 162 | mountComponent = mount(); 163 | }); 164 | 165 | it('should year mode test', () => { 166 | mountComponent.setState({ 167 | viewMode: IDatePicker.ViewMode.YEAR, 168 | }); 169 | 170 | mountComponent 171 | .find('td') 172 | .at(6) 173 | .simulate('click'); 174 | 175 | expect(dayjs(base).format('YYYY')).toEqual('2020'); 176 | expect(mountComponent.state().viewMode).toEqual(IDatePicker.ViewMode.MONTH); 177 | }); 178 | 179 | it('should month mode test', () => { 180 | mountComponent.setState({ 181 | viewMode: IDatePicker.ViewMode.MONTH, 182 | }); 183 | mountComponent 184 | .find('td') 185 | .at(1) 186 | .simulate('click'); 187 | 188 | expect(dayjs(base).format('MM')).toEqual('02'); 189 | expect(mountComponent.state().viewMode).toEqual(IDatePicker.ViewMode.DAY); 190 | }); 191 | 192 | it('should day mode test', () => { 193 | mountComponent.setState({ 194 | viewMode: IDatePicker.ViewMode.DAY, 195 | }); 196 | 197 | mountComponent 198 | .find('td') 199 | .at(6) 200 | .simulate('click'); 201 | 202 | expect(onChange).toHaveProperty('callCount', 1); 203 | }); 204 | }); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /test/CalendarHead.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount, shallow, ShallowWrapper } from 'enzyme'; 2 | import * as React from 'react'; 3 | import * as sinon from 'sinon'; 4 | import CalendarHead from '../src/components/CalendarHead'; 5 | 6 | describe('', () => { 7 | let shallowComponent: ShallowWrapper; 8 | 9 | it('should render correctly', () => { 10 | shallowComponent = shallow(); 11 | 12 | expect(shallowComponent).toBeTruthy(); 13 | expect(shallowComponent).toMatchSnapshot(); 14 | }); 15 | 16 | it('should props title correctly', () => { 17 | shallowComponent = shallow(); 18 | 19 | expect(shallowComponent).toMatchSnapshot(); 20 | expect(shallowComponent.find('h2.calendar__head--title').text()).toEqual('2019/01'); 21 | }); 22 | 23 | it('should props prevIcon, nextIcon correctly', () => { 24 | shallowComponent = shallow(); 25 | 26 | expect(shallowComponent).toMatchSnapshot(); 27 | expect(shallowComponent.find('div.calendar__head--prev > button')).toHaveLength(1); 28 | expect(shallowComponent.find('div.calendar__head--next > button')).toHaveLength(1); 29 | }); 30 | 31 | it('should props onPrev, onNext correctly', () => { 32 | const onPrev = sinon.spy(); 33 | const onNext = sinon.spy(); 34 | 35 | shallowComponent = shallow(); 36 | 37 | expect(shallowComponent).toMatchSnapshot(); 38 | shallowComponent.find('div.calendar__head--prev > button').simulate('click'); 39 | shallowComponent.find('div.calendar__head--next > button').simulate('click'); 40 | 41 | expect(onPrev).toHaveProperty('callCount', 1); 42 | expect(onNext).toHaveProperty('callCount', 1); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/DOMUtil.test.ts: -------------------------------------------------------------------------------- 1 | import { getDivPosition } from '../src/utils/DOMUtil'; 2 | import { IDatePicker } from '../src/common/@types'; 3 | import { mock, instance, when } from 'ts-mockito'; 4 | 5 | describe('utils.DOMUtil', () => { 6 | let mockDiv: HTMLDivElement; 7 | let instanceDiv: HTMLDivElement; 8 | 9 | describe('getDivPosition', () => { 10 | it('works with if direction top', () => { 11 | // given 12 | mockDiv = mock(HTMLDivElement); 13 | when(mockDiv.offsetLeft).thenReturn(10); 14 | when(mockDiv.offsetHeight).thenReturn(10); 15 | when(mockDiv.offsetTop).thenReturn(10); 16 | instanceDiv = instance(mockDiv); 17 | 18 | // when 19 | const position = getDivPosition(instanceDiv, IDatePicker.PickerDirection.TOP, 30); 20 | 21 | // then 22 | expect(position.top).toEqual('-25px'); 23 | expect(position.left).toEqual('10px'); 24 | }); 25 | 26 | it('works with if direction bottom', () => { 27 | // given 28 | mockDiv = mock(HTMLDivElement); 29 | when(mockDiv.offsetLeft).thenReturn(10); 30 | when(mockDiv.offsetHeight).thenReturn(10); 31 | when(mockDiv.offsetTop).thenReturn(10); 32 | instanceDiv = instance(mockDiv); 33 | 34 | // when 35 | const position = getDivPosition(instanceDiv, IDatePicker.PickerDirection.BOTTOM, 30); 36 | 37 | // then 38 | expect(position.top).toEqual('25px'); 39 | expect(position.left).toEqual('10px'); 40 | }); 41 | 42 | it('does calculate distance if distance parameter not default', () => { 43 | // given 44 | mockDiv = mock(HTMLDivElement); 45 | when(mockDiv.offsetLeft).thenReturn(10); 46 | when(mockDiv.offsetHeight).thenReturn(10); 47 | when(mockDiv.offsetTop).thenReturn(10); 48 | instanceDiv = instance(mockDiv); 49 | 50 | // when 51 | const position = getDivPosition(instanceDiv, IDatePicker.PickerDirection.BOTTOM, 10, 10); 52 | 53 | // then 54 | expect(position.top).toEqual('30px'); 55 | expect(position.left).toEqual('10px'); 56 | }); 57 | 58 | it('if node null return empty position object', () => { 59 | // given 60 | const nullNode = null; 61 | 62 | // when 63 | const position = getDivPosition(nullNode, IDatePicker.PickerDirection.BOTTOM, 10); 64 | 65 | // then 66 | expect(position.top).toEqual(''); 67 | expect(position.left).toEqual(''); 68 | expect(position.bottom).toEqual(''); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/DatePicker.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount, shallow, ShallowWrapper, ReactWrapper } from 'enzyme'; 2 | import * as dayjs from 'dayjs'; 3 | import * as React from 'react'; 4 | import * as sinon from 'sinon'; 5 | import { DatePickerDefaults } from '../src/common/Constant'; 6 | import Calendar from '../src/components/Calendar'; 7 | import DatePicker, { Props, State, TabValue } from '../src/components/DatePicker'; 8 | 9 | const PICKER_INPUT_CLASS = '.picker-input__text'; 10 | 11 | describe('', () => { 12 | let shallowComponent: ShallowWrapper>; 13 | let mountComponent: ReactWrapper; 14 | 15 | const defaultProps = { 16 | initialDate: dayjs(new Date(2018, 11, 1)), 17 | }; 18 | 19 | const pickerShow = (component: ReactWrapper) => { 20 | component.find('Picker').setState({ 21 | show: true, 22 | }); 23 | }; 24 | const daySelect = (at: number) => { 25 | mountComponent 26 | .find('td') 27 | .at(at) 28 | .simulate('click'); 29 | }; 30 | 31 | describe('shallow render test', () => { 32 | beforeEach(() => { 33 | shallowComponent = shallow(); 34 | }); 35 | 36 | it('renders with no props', () => { 37 | expect(shallowComponent).toBeTruthy(); 38 | }); 39 | 40 | it('should includeTime & showTimeOnly cannot be used together', () => { 41 | expect(() => { 42 | shallowComponent = shallow(); 43 | }).toThrowError(Error); 44 | }); 45 | 46 | describe('dateFormat', () => { 47 | it('should includeTime dateFormat correctly', () => { 48 | const shallowDatePicker = shallow(); 49 | expect(shallowDatePicker.instance().getDateFormat()).toBe( 50 | DatePickerDefaults.dateTimeFormat 51 | ); 52 | }); 53 | 54 | it('should calendarOnly dateFormat correctly', () => { 55 | const shallowDatePicker = shallow(); 56 | expect(shallowDatePicker.instance().getDateFormat()).toBe(DatePickerDefaults.dateFormat); 57 | }); 58 | 59 | it('should timeOnly dateFormat correctly', () => { 60 | const shallowDatePicker = shallow( 61 | 62 | ); 63 | expect(shallowDatePicker.instance().getDateFormat()).toBe(DatePickerDefaults.timeFormat); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('mount test', () => { 69 | let onChange: sinon.SinonSpy; 70 | beforeEach(() => { 71 | onChange = sinon.spy(); 72 | mountComponent = mount( 73 | 74 | ); 75 | pickerShow(mountComponent); 76 | }); 77 | 78 | it('should showTimeonly render only timeContainer', () => { 79 | mountComponent = mount(); 80 | pickerShow(mountComponent); 81 | expect(mountComponent.find('.picker__container__timeonly')).toHaveLength(1); 82 | }); 83 | 84 | it('should input ref correctly', () => { 85 | mountComponent.find('.picker__trigger').simulate('click'); 86 | expect(mountComponent.find('Calendar')).toHaveLength(1); 87 | }); 88 | 89 | it('should props dateFormat correctly', () => { 90 | daySelect(7); 91 | expect(mountComponent.find('.picker__trigger input').prop('value')).toEqual('2018/12/02'); 92 | }); 93 | 94 | it('should props onChange correctly', () => { 95 | daySelect(6); 96 | expect(onChange).toHaveProperty('callCount', 1); 97 | expect(mountComponent.state('calendarShow')).toBeFalsy(); 98 | expect(mountComponent.state('selected')).toHaveLength(1); 99 | }); 100 | 101 | it('should props showMonthCnt correctly', () => { 102 | // 20180501 103 | const mockDate = dayjs(new Date(2018, 5, 1)); 104 | mountComponent = mount(); 105 | pickerShow(mountComponent); 106 | expect(mountComponent.find('.calendar__container')).toHaveLength(3); 107 | }); 108 | 109 | it('should input interaction correctly', () => { 110 | mountComponent.find('.picker__trigger').simulate('click'); 111 | expect(mountComponent.find('.rc-backdrop')).toHaveLength(1); 112 | expect(mountComponent.find(Calendar)).toBeTruthy(); 113 | expect(mountComponent.find('Picker').state('show')).toBeTruthy(); 114 | }); 115 | 116 | it('should hideCalendar correctly', () => { 117 | mountComponent.find('.picker__trigger').simulate('click'); 118 | mountComponent.find('.rc-backdrop').simulate('click'); 119 | expect(mountComponent.find('Picker').state('show')).toBeFalsy(); 120 | }); 121 | }); 122 | 123 | describe('include time', () => { 124 | beforeEach(() => { 125 | mountComponent = mount(); 126 | }); 127 | 128 | it('should time container render correctly', () => { 129 | expect(mountComponent.find('.time__container')).toBeTruthy(); 130 | }); 131 | 132 | it('should tab TIME click correctly', () => { 133 | mountComponent.setState({ 134 | ...mountComponent.state, 135 | tabValue: TabValue.DATE, 136 | }); 137 | pickerShow(mountComponent); 138 | 139 | mountComponent 140 | .find('.picker__container__tab button') 141 | .at(1) 142 | .simulate('click'); 143 | expect(mountComponent.state('tabValue')).toEqual(TabValue.TIME); 144 | }); 145 | 146 | describe('handleTimeChange', () => { 147 | const handleClick = (type: string, at: number) => { 148 | mountComponent 149 | .find(`.time-input__${type} button`) 150 | .at(at) 151 | .simulate('click'); 152 | }; 153 | 154 | beforeEach(() => { 155 | mountComponent = mount(); 156 | mountComponent.setState({ 157 | tabValue: TabValue.TIME, 158 | }); 159 | pickerShow(mountComponent); 160 | }); 161 | 162 | it('should set new date when state date is null', () => { 163 | mountComponent = mount(); 164 | mountComponent.setState({ 165 | tabValue: TabValue.TIME, 166 | }); 167 | 168 | pickerShow(mountComponent); 169 | handleClick('up', 0); 170 | 171 | const date = mountComponent.state().date; 172 | 173 | expect(date).toBeDefined(); 174 | if (date) { 175 | expect(date.format(DatePickerDefaults.dateFormat)).toBe( 176 | dayjs().format(DatePickerDefaults.dateFormat) 177 | ); 178 | } 179 | }); 180 | 181 | it('should time change set date & input value correctly', () => { 182 | mountComponent = mount(); 183 | mountComponent.setState({ 184 | tabValue: TabValue.TIME, 185 | }); 186 | 187 | pickerShow(mountComponent); 188 | handleClick('up', 0); 189 | const date = mountComponent.state().date; 190 | if (date) { 191 | expect(date.format('YYYYMMDDHHmm')).toBe('201904032320'); 192 | } 193 | }); 194 | 195 | it('should fire onChange Event', () => { 196 | const onChange = sinon.spy(); 197 | mountComponent = mount(); 198 | mountComponent.setState({ 199 | tabValue: TabValue.TIME, 200 | }); 201 | pickerShow(mountComponent); 202 | handleClick('up', 0); 203 | expect(onChange).toHaveProperty('callCount', 1); 204 | }); 205 | }); 206 | }); 207 | 208 | describe('handleInputBlur', () => { 209 | beforeEach(() => { 210 | mountComponent = mount(); 211 | }); 212 | 213 | it('should date picker input invalid value return original date', () => { 214 | const { dateFormat } = DatePickerDefaults; 215 | const originalDate = dayjs(new Date(2018, 4, 1)); 216 | const testValue = 'teste333'; 217 | mountComponent.setState({ 218 | ...mountComponent.state, 219 | date: originalDate, 220 | inputValue: testValue, 221 | }); 222 | 223 | mountComponent.find(PICKER_INPUT_CLASS).simulate('blur'); 224 | 225 | const dateState = mountComponent.state('date'); 226 | expect(dateState).not.toBeUndefined(); 227 | 228 | if (dateState) { 229 | expect(dayjs(dateState).format(dateFormat)).toEqual(dayjs(originalDate).format(dateFormat)); 230 | } 231 | }); 232 | 233 | it('should date picker input valid value setState date', () => { 234 | const { dateFormat } = DatePickerDefaults; 235 | const originalDate = dayjs(new Date(2018, 4, 1)); 236 | const correctValue = '2018-05-02'; 237 | mountComponent.setState({ 238 | ...mountComponent.state, 239 | date: originalDate, 240 | inputValue: correctValue, 241 | }); 242 | 243 | mountComponent.find(PICKER_INPUT_CLASS).simulate('blur'); 244 | 245 | const dateState = mountComponent.state('date'); 246 | expect(dateState).not.toBeUndefined(); 247 | 248 | if (dateState) { 249 | expect(dayjs(dateState).format(dateFormat)).toEqual(correctValue); 250 | } 251 | }); 252 | 253 | it('should time picker input invalid value return original time', () => { 254 | mountComponent = mount(); 255 | const originalDate = dayjs('201904031022'); 256 | 257 | mountComponent.setState({ 258 | ...mountComponent.state, 259 | date: originalDate, 260 | inputValue: `${dayjs(originalDate).format(DatePickerDefaults.dateFormat)} 03:e0A`, 261 | }); 262 | 263 | mountComponent.find(PICKER_INPUT_CLASS).simulate('blur'); 264 | 265 | const inputValue = mountComponent.state('inputValue'); 266 | 267 | expect(inputValue).toBe(originalDate.format(DatePickerDefaults.dateTimeFormat)); 268 | }); 269 | }); 270 | 271 | describe('handleInputClear', () => { 272 | beforeEach(() => { 273 | mountComponent = mount(); 274 | }); 275 | 276 | it('should clear button render correctly', () => { 277 | expect(mountComponent.find('.icon-clear').first()).toHaveLength(1); 278 | }); 279 | 280 | it('should clear click state change correctly', () => { 281 | mountComponent 282 | .find('.icon-clear') 283 | .first() 284 | .simulate('click'); 285 | expect(mountComponent.state('inputValue')).toEqual(''); 286 | }); 287 | 288 | it('should clear click onChange fired!', () => { 289 | const onChange = sinon.spy(); 290 | mountComponent = mount(); 291 | mountComponent 292 | .find('.icon-clear') 293 | .first() 294 | .simulate('click'); 295 | expect(onChange).toHaveProperty('callCount', 1); 296 | }); 297 | }); 298 | 299 | describe('handleInputChange', () => { 300 | it('should input change correctly', () => { 301 | const onChange = sinon.spy(); 302 | mountComponent = mount(); 303 | mountComponent.find(PICKER_INPUT_CLASS).simulate('change'); 304 | expect(onChange).toHaveProperty('callCount', 1); 305 | mountComponent = mount(); 306 | mountComponent.find(PICKER_INPUT_CLASS).simulate('change'); 307 | }); 308 | }); 309 | 310 | describe('input props', () => { 311 | it('should input disabled do not run handleCalendar', () => { 312 | mountComponent = mount(); 313 | mountComponent.find(PICKER_INPUT_CLASS).simulate('click'); 314 | expect(mountComponent.state('show')).toBeFalsy(); 315 | }); 316 | 317 | it('should showDefaultIcon correctly', () => { 318 | mountComponent = mount(); 319 | expect(mountComponent.find('.icon-calendar').first()).toHaveLength(1); 320 | }); 321 | }); 322 | 323 | describe('custom input test', () => { 324 | beforeEach(() => { 325 | const customInputComponent = (props: any) =>