├── .babelrc ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── node-pretest.yml │ ├── node.yml │ ├── rebase.yml │ └── require-allow-edits.yml ├── .gitignore ├── .lgtm ├── .npmignore ├── .npmrc ├── .nycrc ├── .storybook-css ├── .eslintrc ├── addons.js ├── config.js └── webpack.config.js ├── .storybook ├── .eslintrc ├── addons.js ├── config.js └── webpack.config.js ├── .travis.yml ├── CHANGELOG.md ├── INTHEWILD.md ├── LICENSE ├── MAINTAINERS ├── README.md ├── constants.js ├── css └── storybook.scss ├── examples ├── .eslintrc ├── DateRangePickerWrapper.jsx ├── DayPickerRangeControllerWrapper.jsx ├── DayPickerSingleDateControllerWrapper.jsx ├── PresetDateRangePicker.jsx └── SingleDatePickerWrapper.jsx ├── index.js ├── initialize.js ├── karma.conf.js ├── package.json ├── react-dates-demo.gif ├── scripts ├── .eslintrc ├── buildCSS.js ├── pure-component-fallback.js └── renderAllComponents.jsx ├── src ├── components │ ├── CalendarDay.jsx │ ├── CalendarIcon.jsx │ ├── CalendarMonth.jsx │ ├── CalendarMonthGrid.jsx │ ├── CalendarWeek.jsx │ ├── ChevronDown.jsx │ ├── ChevronUp.jsx │ ├── CloseButton.jsx │ ├── CustomizableCalendarDay.jsx │ ├── DateInput.jsx │ ├── DateRangePicker.jsx │ ├── DateRangePickerInput.jsx │ ├── DateRangePickerInputController.jsx │ ├── DayPicker.jsx │ ├── DayPickerKeyboardShortcuts.jsx │ ├── DayPickerNavigation.jsx │ ├── DayPickerRangeController.jsx │ ├── DayPickerSingleDateController.jsx │ ├── KeyboardShortcutRow.jsx │ ├── LeftArrow.jsx │ ├── RightArrow.jsx │ ├── SingleDatePicker.jsx │ ├── SingleDatePickerInput.jsx │ └── SingleDatePickerInputController.jsx ├── constants.js ├── defaultPhrases.js ├── index.js ├── initialize.js ├── shapes │ ├── AnchorDirectionShape.js │ ├── CalendarInfoPositionShape.js │ ├── DateRangePickerShape.js │ ├── DayOfWeekShape.js │ ├── DisabledShape.js │ ├── FocusedInputShape.js │ ├── IconPositionShape.js │ ├── ModifiersShape.js │ ├── NavPositionShape.js │ ├── OpenDirectionShape.js │ ├── OrientationShape.js │ ├── ScrollableOrientationShape.js │ └── SingleDatePickerShape.js ├── svg │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── calendar.svg │ ├── chevron-down.svg │ ├── chevron-up.svg │ └── close.svg ├── theme │ └── DefaultTheme.js └── utils │ ├── calculateDimension.js │ ├── disableScroll.js │ ├── getActiveElement.js │ ├── getCalendarDaySettings.js │ ├── getCalendarMonthWeeks.js │ ├── getCalendarMonthWidth.js │ ├── getDetachedContainerStyles.js │ ├── getInputHeight.js │ ├── getNumberOfCalendarMonthWeeks.js │ ├── getPhrase.jsx │ ├── getPhrasePropTypes.js │ ├── getPooledMoment.js │ ├── getPreviousMonthMemoLast.js │ ├── getResponsiveContainerStyles.js │ ├── getSelectedDateOffset.js │ ├── getTransformStyles.js │ ├── getVisibleDays.js │ ├── isAfterDay.js │ ├── isBeforeDay.js │ ├── isDayVisible.js │ ├── isInclusivelyAfterDay.js │ ├── isInclusivelyBeforeDay.js │ ├── isNextDay.js │ ├── isNextMonth.js │ ├── isPrevMonth.js │ ├── isPreviousDay.js │ ├── isSameDay.js │ ├── isSameMonth.js │ ├── isTransitionEndSupported.js │ ├── modifiers.js │ ├── noflip.js │ ├── registerCSSInterfaceWithDefaultTheme.js │ ├── registerInterfaceWithDefaultTheme.js │ ├── toISODateString.js │ ├── toISOMonthString.js │ ├── toLocalizedDateString.js │ └── toMomentObject.js ├── stories ├── .eslintrc ├── DateRangePicker.js ├── DateRangePicker_calendar.js ├── DateRangePicker_day.js ├── DateRangePicker_input.js ├── DayPicker.js ├── DayPickerRangeController.js ├── DayPickerSingleDateController.js ├── InfoPanelDecorator.js ├── PresetDateRangePicker.js ├── SingleDatePicker.js ├── SingleDatePicker_calendar.js ├── SingleDatePicker_day.js ├── SingleDatePicker_input.js └── withStyles.js └── test ├── _helpers ├── describeIfWindow.js ├── enzymeSetup.js ├── registerReactWithStylesInterface.js ├── restoreSinonStubs.js └── withTouchSupport.js ├── browser-main.js ├── components ├── CalendarDay_spec.jsx ├── CalendarMonthGrid_spec.jsx ├── CalendarMonth_spec.jsx ├── CalendarWeek_spec.jsx ├── CustomizableCalendarDay_spec.jsx ├── DateInput_spec.jsx ├── DateRangePickerInputController_spec.jsx ├── DateRangePickerInput_spec.jsx ├── DateRangePicker_spec.jsx ├── DayPickerKeyboardShortcuts_spec.jsx ├── DayPickerNavigation_spec.jsx ├── DayPickerRangeController_spec.jsx ├── DayPickerSingleDateController_spec.jsx ├── DayPicker_spec.jsx ├── KeyboardShortcutRow_spec.jsx ├── SingleDatePickerInputController_spec.jsx ├── SingleDatePickerInput_spec.jsx └── SingleDatePicker_spec.jsx ├── mocha.opts └── utils ├── calculateDimension_spec.js ├── disableScroll_spec.js ├── getActiveElement_spec.js ├── getCalendarDaySettings_spec.js ├── getCalendarMonthWeeks_spec.js ├── getCalendarMonthWidth_spec.js ├── getDetachedContainerStyles_spec.js ├── getInputHeight_spec.js ├── getNumberOfCalendarMonthWeeks_spec.js ├── getPhrasePropTypes_spec.js ├── getPhrase_spec.js ├── getPooledMoment_spec.js ├── getResponsiveContainerStyles_spec.js ├── getSelectedDateOffset_spec.js ├── getTransformStyles_spec.js ├── getVisibleDays_spec.js ├── isAfterDay_spec.js ├── isBeforeDay_spec.js ├── isDayVisible_spec.js ├── isInclusivelyAfterDay_spec.js ├── isInclusivelyBeforeDay_spec.js ├── isNextDay_spec.js ├── isNextMonth_spec.js ├── isPrevMonth_spec.js ├── isPreviousDay_spec.js ├── isSameDay_spec.js ├── isSameMonth_spec.js ├── isTransitionEndSupported_spec.js ├── noflip_spec.js ├── toISODateString_spec.js ├── toISOMonthString_spec.js ├── toLocalizedDateString_spec.js └── toMomentObject_spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [["airbnb", { looseClasses: true }]], 5 | "plugins": [ 6 | ["import-path-replace", { 7 | "rules": [ 8 | { 9 | "match": "../src/", 10 | "replacement": "../lib/" 11 | } 12 | ] 13 | }], 14 | ["inline-react-svg", { 15 | "svgo": false 16 | }], 17 | ["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }], 18 | "./scripts/pure-component-fallback.js", 19 | "istanbul", 20 | ] 21 | }, 22 | 23 | "development": { 24 | "presets": [["airbnb", { looseClasses: true }]], 25 | "plugins": [ 26 | ["inline-react-svg", { 27 | "svgo": false 28 | }], 29 | ["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }], 30 | "./scripts/pure-component-fallback.js", 31 | ], 32 | }, 33 | 34 | "production": { 35 | "presets": [["airbnb", { looseClasses: true, removePropTypes: true }]], 36 | "plugins": [ 37 | ["inline-react-svg", { 38 | "svgo": false 39 | }], 40 | ["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }], 41 | "./scripts/pure-component-fallback.js", 42 | ], 43 | }, 44 | 45 | "cjs": { 46 | "presets": [["airbnb", { looseClasses: true, removePropTypes: true }]], 47 | "plugins": [ 48 | ["inline-react-svg", { 49 | "svgo": false 50 | }], 51 | ["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }], 52 | "./scripts/pure-component-fallback.js", 53 | ], 54 | }, 55 | 56 | "esm": { 57 | "presets": [["airbnb", { looseClasses: true, modules: false, removePropTypes: true }]], 58 | "plugins": [ 59 | ["inline-react-svg", { 60 | "svgo": false 61 | }], 62 | ["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }], 63 | "./scripts/pure-component-fallback.js", 64 | ], 65 | }, 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | 4 | "extends": [ 5 | "airbnb", 6 | "plugin:react-with-styles/recommended", 7 | ], 8 | 9 | "plugins": [ 10 | "react-with-styles", 11 | ], 12 | 13 | "env": { 14 | "browser": true, 15 | "node": true, 16 | }, 17 | 18 | "ignorePatterns": [ 19 | "lib/", 20 | ".storybook/", 21 | "test/_helpers/", 22 | "webpack.config.js", 23 | ], 24 | 25 | "parser": "@babel/eslint-parser", 26 | 27 | "rules": { 28 | "max-len": "off", 29 | 30 | "react/forbid-foreign-prop-types": 2, // For babel-plugin-transform-react-remove-prop-types 31 | 32 | "jsx-a11y/click-events-have-key-events": 1, // TODO: enable 33 | 34 | "react-with-styles/no-unused-styles": 2, 35 | 36 | "no-restricted-imports": 0, // TODO: enable with full RTL support 37 | 38 | "react/jsx-props-no-spreading": 0, // TODO: re-evaluate 39 | 40 | "react/no-deprecated": 1, // TODO: update to UNSAFE_componentWillReceiveProps 41 | }, 42 | 43 | "overrides": [ 44 | { 45 | "files": "test/**/*", 46 | "env": { 47 | "mocha": true, 48 | }, 49 | "extends": "airbnb", 50 | "rules": { 51 | "react/jsx-props-no-spreading": 0, 52 | //"import/no-extraneous-dependencies": [2, { 53 | //"devDependencies": true 54 | //}], 55 | "indent": [2, 2, { 56 | "MemberExpression": "off", 57 | }], 58 | "react/function-component-definition": "off", 59 | }, 60 | }, 61 | ], 62 | 63 | "settings": { 64 | "propWrapperFunctions": ["forbidExtraProps", "exact", "Object.freeze"], 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /.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 | **react-dates version** 11 | e.g. react-dates@18.3.1 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **Source code (including props configuration)** 17 | Steps to reproduce the behavior: 18 | ``` 19 | <DateRangePicker 20 | startDate={this.state.startDate} 21 | startDateId="your_unique_start_date_id" 22 | endDate={this.state.endDate} 23 | endDateId="your_unique_end_date_id" 24 | onDatesChange={({ startDate, endDate }) => this.setState({ startDate, endDate })} 25 | focusedInput={this.state.focusedInput} 26 | onFocusChange={focusedInput => this.setState({ focusedInput })} 27 | /> 28 | ``` 29 | If you have custom methods that you are passing into a `react-dates` component, e.g. `onDatesChange`, `onFocusChange`, `renderMonth`, `isDayBlocked`, etc., please include the source for those as well. 30 | 31 | **Screenshots/Gifs** 32 | If applicable, add screenshots or gifs to help explain your problem. 33 | 34 | **Desktop (please complete the following information):** 35 | - OS: [e.g. iOS] 36 | - Browser [e.g. chrome, safari] 37 | - Version [e.g. 22] 38 | 39 | **Smartphone (please complete the following information):** 40 | - Device: [e.g. iPhone6] 41 | - OS: [e.g. iOS8.1] 42 | - Browser [e.g. stock browser, safari] 43 | - Version [e.g. 22] 44 | 45 | **Is the issue reproducible in Storybook?** 46 | Please link to the relevant storybook example 47 | 48 | **Additional context** 49 | Add any other context about the problem here. 50 | -------------------------------------------------------------------------------- /.github/workflows/node-pretest.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: pretest/posttest/build' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | pretest: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: ljharb/actions/node/install@main 12 | name: 'nvm install lts/* && npm install' 13 | with: 14 | node-version: 'lts/*' 15 | - run: npm run pretest 16 | 17 | # posttest: 18 | # runs-on: ubuntu-latest 19 | 20 | # steps: 21 | # - uses: actions/checkout@v2 22 | # - uses: ljharb/actions/node/install@main 23 | # name: 'nvm install lts/* && npm install' 24 | # with: 25 | # node-version: 'lts/*' 26 | # - run: npm run posttest 27 | 28 | build: 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: ljharb/actions/node/install@main 34 | name: 'nvm install lts/* && npm install' 35 | with: 36 | node-version: 'lts/*' 37 | - run: npm run build 38 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: node.js' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | matrix: 7 | runs-on: ubuntu-latest 8 | outputs: 9 | latest: ${{ steps.set-matrix.outputs.requireds }} 10 | minors: ${{ steps.set-matrix.outputs.optionals }} 11 | steps: 12 | - uses: ljharb/actions/node/matrix@main 13 | id: set-matrix 14 | with: 15 | type: 'majors' 16 | versionsAsRoot: true 17 | preset: '>=4' 18 | 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: ljharb/actions/node/install@main 24 | with: 25 | node-version: 'lts/*' 26 | - uses: actions/cache@v2 27 | id: cache 28 | with: 29 | path: | 30 | lib 31 | esm 32 | test-build 33 | key: ${{ runner.os }}-${{ hashFiles('package.json', 'src/**', 'test/**', 'scripts/buildCSS.js') }} 34 | - run: npm run build 35 | if: steps.cache.outputs.cache-hit != 'true' 36 | - run: npm run build:test 37 | if: steps.cache.outputs.cache-hit != 'true' 38 | 39 | 40 | majors: 41 | needs: [build, matrix] 42 | name: 'latest minors' 43 | runs-on: ubuntu-latest 44 | 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | node-version: ${{ fromJson(needs.matrix.outputs.latest) }} 49 | react: 50 | - '16' 51 | - '16.9' 52 | - '16.3' 53 | - '16.0' 54 | - '15' 55 | - '15.5' 56 | - '15.0' 57 | - '0.14' 58 | 59 | steps: 60 | - uses: actions/checkout@v2 61 | - uses: ljharb/actions/node/install@main 62 | name: 'nvm install ${{ matrix.node-version }} && npm install' 63 | with: 64 | node-version: ${{ matrix.node-version }} 65 | skip-ls-check: ${{ !startsWith(matrix.node-version, '5.') && !startsWith(matrix.node-version, '4.') && 'true' || 'false' }} 66 | - uses: actions/cache@v2 67 | id: cache 68 | with: 69 | path: | 70 | lib 71 | esm 72 | test-build 73 | key: ${{ runner.os }}-${{ hashFiles('package.json', 'src/**', 'test/**', 'scripts/buildCSS.js') }} 74 | - run: npm run react 75 | env: 76 | REACT: ${{ matrix.react }} 77 | - run: npm run tests-only 78 | env: 79 | REACT: ${{ matrix.react }} 80 | - uses: codecov/codecov-action@v2 81 | 82 | node: 83 | name: 'node.js' 84 | needs: [majors] 85 | runs-on: ubuntu-latest 86 | steps: 87 | - run: 'echo tests completed' 88 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Rebase 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | _: 7 | name: "Automatic Rebase" 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: ljharb/rebase@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/require-allow-edits.yml: -------------------------------------------------------------------------------- 1 | name: Require “Allow Edits” 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | _: 7 | name: "Require “Allow Edits”" 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: ljharb/require-allow-edits@main 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # build folder 36 | lib 37 | esm 38 | test-build 39 | .idea 40 | 41 | # gh-pages 42 | _gh-pages 43 | 44 | .nyc_output 45 | 46 | # Only apps should have lockfiles 47 | yarn.lock 48 | package-lock.json 49 | 50 | css/styles.css 51 | 52 | # Cache 53 | .cache/ 54 | -------------------------------------------------------------------------------- /.lgtm: -------------------------------------------------------------------------------- 1 | approvals = 1 2 | pattern = "(?i):shipit:|:\\+1:|LGTM" 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | .cache 36 | .github 37 | .lgtm 38 | .storybook 39 | .storybook-css 40 | _gh-pages 41 | examples 42 | MAINTAINERS 43 | public 44 | react-dates-demo.gif 45 | stories 46 | test 47 | test-build 48 | css 49 | !lib/css 50 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".js", 4 | ".jsx" 5 | ], 6 | "include": [ 7 | "src" 8 | ], 9 | "require": [ 10 | ], 11 | "reporter": [ 12 | "text", 13 | "html", 14 | "lcov" 15 | ], 16 | "all": true, 17 | "sourceMap": false, 18 | "instrument": false 19 | } 20 | -------------------------------------------------------------------------------- /.storybook-css/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": [2, { 4 | "devDependencies": true 5 | }], 6 | "global-require": 2, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.storybook-css/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook-css/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | 4 | import { configure, addDecorator, setAddon } from '@storybook/react'; 5 | import infoAddon from '@storybook/addon-info'; 6 | import { setOptions } from '@storybook/addon-options'; 7 | 8 | import registerCSSInterfaceWithDefaultTheme from '../src/utils/registerCSSInterfaceWithDefaultTheme'; 9 | 10 | import '../css/storybook.scss'; 11 | import '../css/styles.css'; 12 | 13 | registerCSSInterfaceWithDefaultTheme(); 14 | 15 | addDecorator((story) => { 16 | moment.locale('en'); 17 | return story(); 18 | }); 19 | 20 | function getLink(href, text) { 21 | return `<a href=${href} rel="noopener noreferrer" target="_blank">${text}</a>`; 22 | } 23 | 24 | const README = getLink('https://github.com/react-dates/react-dates/blob/HEAD/README.md', 'README'); 25 | const wrapperSource = getLink('https://github.com/react-dates/react-dates/tree/HEAD/examples', 'wrapper source'); 26 | 27 | const helperText = `All examples are built using a wrapper component that is not exported by 28 | react-dates. Please see the ${README} for more information about minimal setup or explore 29 | the ${wrapperSource} to see how to integrate react-dates into your own app.`; 30 | 31 | addDecorator(story => ( 32 | <div> 33 | <div 34 | style={{ 35 | background: '#fff', 36 | height: 6 * 8, 37 | width: '100%', 38 | position: 'fixed', 39 | top: 0, 40 | left: 0, 41 | padding: '8px 40px 8px 8px', 42 | overflow: 'scroll', 43 | }} 44 | dangerouslySetInnerHTML={{ __html: helperText }} 45 | /> 46 | 47 | <div style={{ marginTop: 7 * 8 }}> 48 | {story()} 49 | </div> 50 | </div> 51 | )); 52 | 53 | setOptions({ 54 | name: 'REACT-DATES', 55 | url: 'https://github.com/react-dates/react-dates', 56 | }); 57 | 58 | function loadStories() { 59 | require('../stories/DateRangePicker'); 60 | require('../stories/DateRangePicker_input'); 61 | require('../stories/DateRangePicker_calendar'); 62 | require('../stories/DateRangePicker_day'); 63 | require('../stories/SingleDatePicker'); 64 | require('../stories/SingleDatePicker_input'); 65 | require('../stories/SingleDatePicker_calendar'); 66 | require('../stories/SingleDatePicker_day'); 67 | require('../stories/DayPickerRangeController'); 68 | require('../stories/DayPickerSingleDateController'); 69 | require('../stories/DayPicker'); 70 | require('../stories/PresetDateRangePicker'); 71 | } 72 | 73 | setAddon(infoAddon); 74 | 75 | configure(loadStories, module); 76 | -------------------------------------------------------------------------------- /.storybook-css/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.s?css$/, 8 | use: ['style-loader', 'raw-loader', 'sass-loader'], 9 | include: [ 10 | path.resolve(__dirname, '../css/'), 11 | /@storybook\/addon-info/, 12 | ], 13 | }, 14 | { 15 | test: /\.svg$/, 16 | use: [ 17 | { 18 | loader: 'babel-loader', 19 | query: { 20 | presets: ['airbnb'], 21 | }, 22 | }, 23 | ], 24 | }, 25 | { 26 | test: /\.jsx$/, 27 | use: [ 28 | { 29 | loader: 'babel-loader', 30 | query: { 31 | presets: ['airbnb'], 32 | }, 33 | }, 34 | ], 35 | }, 36 | ], 37 | }, 38 | resolve: { 39 | extensions: ['.js', '.jsx'], 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /.storybook/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": [2, { 4 | "devDependencies": true 5 | }], 6 | "global-require": 2, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | if (process.env.NODE_ENV !== 'production') { 4 | const whyDidYouRender = require('@welldone-software/why-did-you-render'); 5 | whyDidYouRender(React); 6 | } 7 | 8 | import moment from 'moment'; 9 | import aphroditeInterface from 'react-with-styles-interface-aphrodite'; 10 | 11 | import { configure, addDecorator, setAddon } from '@storybook/react'; 12 | import infoAddon from '@storybook/addon-info'; 13 | import { setOptions } from '@storybook/addon-options'; 14 | 15 | import registerInterfaceWithDefaultTheme from '../src/utils/registerInterfaceWithDefaultTheme'; 16 | 17 | import '../css/storybook.scss'; 18 | 19 | registerInterfaceWithDefaultTheme(aphroditeInterface); 20 | 21 | addDecorator((story) => { 22 | moment.locale('en'); 23 | return story(); 24 | }); 25 | 26 | function getLink(href, text) { 27 | return `<a href=${href} rel="noopener noreferrer" target="_blank">${text}</a>`; 28 | } 29 | 30 | const README = getLink('https://github.com/react-dates/react-dates/blob/HEAD/README.md', 'README'); 31 | const wrapperSource = getLink('https://github.com/react-dates/react-dates/tree/HEAD/examples', 'wrapper source'); 32 | 33 | const helperText = `All examples are built using a wrapper component that is not exported by 34 | react-dates. Please see the ${README} for more information about minimal setup or explore 35 | the ${wrapperSource} to see how to integrate react-dates into your own app.`; 36 | 37 | addDecorator(story => ( 38 | <div> 39 | <div 40 | style={{ 41 | background: '#fff', 42 | height: 6 * 8, 43 | width: '100%', 44 | position: 'fixed', 45 | top: 0, 46 | left: 0, 47 | padding: '8px 40px 8px 8px', 48 | overflow: 'scroll', 49 | }} 50 | dangerouslySetInnerHTML={{ __html: helperText }} 51 | /> 52 | 53 | <div style={{ marginTop: 7 * 8 }}> 54 | {story()} 55 | </div> 56 | </div> 57 | )); 58 | 59 | setOptions({ 60 | name: 'REACT-DATES', 61 | url: 'https://github.com/react-dates/react-dates', 62 | }); 63 | 64 | function loadStories() { 65 | require('../stories/DateRangePicker'); 66 | require('../stories/DateRangePicker_input'); 67 | require('../stories/DateRangePicker_calendar'); 68 | require('../stories/DateRangePicker_day'); 69 | require('../stories/SingleDatePicker'); 70 | require('../stories/SingleDatePicker_input'); 71 | require('../stories/SingleDatePicker_calendar'); 72 | require('../stories/SingleDatePicker_day'); 73 | require('../stories/DayPickerRangeController'); 74 | require('../stories/DayPickerSingleDateController'); 75 | require('../stories/DayPicker'); 76 | require('../stories/PresetDateRangePicker'); 77 | } 78 | 79 | setAddon(infoAddon); 80 | 81 | configure(loadStories, module); 82 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = ({ config }) => { 4 | config.module.rules.push( 5 | { 6 | test: /\.s?css$/, 7 | use: ['style-loader', 'raw-loader', 'sass-loader'], 8 | include: [path.resolve(__dirname, '../css/')], 9 | }, 10 | { 11 | test: /\.svg$/, 12 | use: [ 13 | { 14 | loader: 'babel-loader', 15 | query: { 16 | presets: ['airbnb'], 17 | }, 18 | }, 19 | ], 20 | }, 21 | { 22 | test: /\.jsx$/, 23 | use: [ 24 | { 25 | loader: 'babel-loader', 26 | query: { 27 | presets: ['airbnb'], 28 | }, 29 | }, 30 | ], 31 | }, 32 | ); 33 | config.resolve.extensions = ['.js', '.jsx']; 34 | return config; 35 | }; 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: lts/* 3 | before_install: 4 | - nvm install-latest-npm 5 | services: 6 | - xvfb 7 | script: 8 | - npm run tests-karma 9 | env: 10 | global: 11 | - REACT=16 12 | - DISPLAY=:99.0 13 | sudo: false 14 | -------------------------------------------------------------------------------- /INTHEWILD.md: -------------------------------------------------------------------------------- 1 | Please use [pull requests](https://github.com/react-dates/react-dates/pull/new) to add your organization and/or project to this document! 2 | 3 | Organizations 4 | ---------- 5 | - [Airbnb](https://github.com/airbnb) 6 | - [Стрелочка](https://strelchka.ru) 7 | 8 | Projects 9 | ---------- 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Airbnb 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 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | gdborton 2 | goatslacker 3 | iancmyers 4 | kesne 5 | lelandrichardson 6 | lencioni 7 | ljharb 8 | majapw 9 | mikefowler 10 | mstorus 11 | spikebrehm 12 | wyattdanger 13 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | module.exports = require('./lib/constants'); 3 | -------------------------------------------------------------------------------- /css/storybook.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgba(0, 0, 0, 0.05); 3 | background-image: repeating-linear-gradient(0deg, transparent, transparent 7px, rgba(0, 0, 0, 0.2) 1px, transparent 8px), repeating-linear-gradient(90deg, transparent, transparent 7px, rgba(0, 0, 0, 0.2) 1px, transparent 8px); 4 | background-size: 8px 8px; 5 | } 6 | 7 | html { 8 | box-sizing: border-box; 9 | font-family: Helvetica, "sans-serif"; 10 | font-size: 14px; 11 | } 12 | 13 | *, *:before, *:after { 14 | box-sizing: inherit; 15 | } 16 | 17 | a { 18 | color: #008489; 19 | font-weight: bold; 20 | } 21 | 22 | .foo-bar { 23 | background: red !important; 24 | } 25 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": [2, { 4 | "devDependencies": true 5 | }], 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/DateRangePickerWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import momentPropTypes from 'react-moment-proptypes'; 4 | import moment from 'moment'; 5 | import omit from 'lodash/omit'; 6 | 7 | import DateRangePicker from '../src/components/DateRangePicker'; 8 | 9 | import { DateRangePickerPhrases } from '../src/defaultPhrases'; 10 | import DateRangePickerShape from '../src/shapes/DateRangePickerShape'; 11 | import { 12 | START_DATE, 13 | END_DATE, 14 | HORIZONTAL_ORIENTATION, 15 | ANCHOR_LEFT, 16 | NAV_POSITION_TOP, 17 | OPEN_DOWN, 18 | } from '../src/constants'; 19 | import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; 20 | 21 | const propTypes = { 22 | // example props for the demo 23 | autoFocus: PropTypes.bool, 24 | autoFocusEndDate: PropTypes.bool, 25 | stateDateWrapper: PropTypes.func, 26 | initialStartDate: momentPropTypes.momentObj, 27 | initialEndDate: momentPropTypes.momentObj, 28 | 29 | ...omit(DateRangePickerShape, [ 30 | 'startDate', 31 | 'endDate', 32 | 'onDatesChange', 33 | 'focusedInput', 34 | 'onFocusChange', 35 | ]), 36 | }; 37 | 38 | const defaultProps = { 39 | // example props for the demo 40 | autoFocus: false, 41 | autoFocusEndDate: false, 42 | initialStartDate: null, 43 | initialEndDate: null, 44 | 45 | // input related props 46 | startDateId: START_DATE, 47 | startDatePlaceholderText: 'Start Date', 48 | endDateId: END_DATE, 49 | endDatePlaceholderText: 'End Date', 50 | disabled: false, 51 | required: false, 52 | screenReaderInputMessage: '', 53 | showClearDates: false, 54 | showDefaultInputIcon: false, 55 | customInputIcon: null, 56 | customArrowIcon: null, 57 | customCloseIcon: null, 58 | block: false, 59 | small: false, 60 | regular: false, 61 | autoComplete: 'off', 62 | 63 | // calendar presentation and interaction related props 64 | renderMonthText: null, 65 | orientation: HORIZONTAL_ORIENTATION, 66 | anchorDirection: ANCHOR_LEFT, 67 | horizontalMargin: 0, 68 | withPortal: false, 69 | withFullScreenPortal: false, 70 | initialVisibleMonth: null, 71 | numberOfMonths: 2, 72 | keepOpenOnDateSelect: false, 73 | reopenPickerOnClearDates: false, 74 | isRTL: false, 75 | openDirection: OPEN_DOWN, 76 | 77 | // navigation related props 78 | navPosition: NAV_POSITION_TOP, 79 | navPrev: null, 80 | navNext: null, 81 | onPrevMonthClick() {}, 82 | onNextMonthClick() {}, 83 | onClose() {}, 84 | 85 | // day presentation and interaction related props 86 | renderCalendarDay: undefined, 87 | renderDayContents: null, 88 | minimumNights: 1, 89 | enableOutsideDays: false, 90 | isDayBlocked: () => false, 91 | isOutsideRange: day => !isInclusivelyAfterDay(day, moment()), 92 | isDayHighlighted: () => false, 93 | 94 | // internationalization 95 | displayFormat: () => moment.localeData().longDateFormat('L'), 96 | monthFormat: 'MMMM YYYY', 97 | phrases: DateRangePickerPhrases, 98 | 99 | stateDateWrapper: date => date, 100 | }; 101 | 102 | class DateRangePickerWrapper extends React.Component { 103 | constructor(props) { 104 | super(props); 105 | 106 | let focusedInput = null; 107 | if (props.autoFocus) { 108 | focusedInput = START_DATE; 109 | } else if (props.autoFocusEndDate) { 110 | focusedInput = END_DATE; 111 | } 112 | 113 | this.state = { 114 | focusedInput, 115 | startDate: props.initialStartDate, 116 | endDate: props.initialEndDate, 117 | }; 118 | 119 | this.onDatesChange = this.onDatesChange.bind(this); 120 | this.onFocusChange = this.onFocusChange.bind(this); 121 | } 122 | 123 | onDatesChange({ startDate, endDate }) { 124 | const { stateDateWrapper } = this.props; 125 | this.setState({ 126 | startDate: startDate && stateDateWrapper(startDate), 127 | endDate: endDate && stateDateWrapper(endDate), 128 | }); 129 | } 130 | 131 | onFocusChange(focusedInput) { 132 | this.setState({ focusedInput }); 133 | } 134 | 135 | render() { 136 | const { focusedInput, startDate, endDate } = this.state; 137 | 138 | // autoFocus, autoFocusEndDate, initialStartDate and initialEndDate are helper props for the 139 | // example wrapper but are not props on the SingleDatePicker itself and 140 | // thus, have to be omitted. 141 | const props = omit(this.props, [ 142 | 'autoFocus', 143 | 'autoFocusEndDate', 144 | 'initialStartDate', 145 | 'initialEndDate', 146 | 'stateDateWrapper', 147 | ]); 148 | 149 | return ( 150 | <div> 151 | <DateRangePicker 152 | {...props} 153 | onDatesChange={this.onDatesChange} 154 | onFocusChange={this.onFocusChange} 155 | focusedInput={focusedInput} 156 | startDate={startDate} 157 | endDate={endDate} 158 | /> 159 | </div> 160 | ); 161 | } 162 | } 163 | 164 | DateRangePickerWrapper.propTypes = propTypes; 165 | DateRangePickerWrapper.defaultProps = defaultProps; 166 | 167 | export default DateRangePickerWrapper; 168 | -------------------------------------------------------------------------------- /examples/DayPickerRangeControllerWrapper.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unused-prop-types */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import momentPropTypes from 'react-moment-proptypes'; 5 | import { forbidExtraProps } from 'airbnb-prop-types'; 6 | import moment from 'moment'; 7 | import omit from 'lodash/omit'; 8 | 9 | import DayPickerRangeController from '../src/components/DayPickerRangeController'; 10 | 11 | import ScrollableOrientationShape from '../src/shapes/ScrollableOrientationShape'; 12 | 13 | import { START_DATE, END_DATE, HORIZONTAL_ORIENTATION } from '../src/constants'; 14 | import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; 15 | 16 | const propTypes = forbidExtraProps({ 17 | // example props for the demo 18 | autoFocusEndDate: PropTypes.bool, 19 | initialStartDate: momentPropTypes.momentObj, 20 | initialEndDate: momentPropTypes.momentObj, 21 | startDateOffset: PropTypes.func, 22 | endDateOffset: PropTypes.func, 23 | showInputs: PropTypes.bool, 24 | minDate: momentPropTypes.momentObj, 25 | maxDate: momentPropTypes.momentObj, 26 | 27 | keepOpenOnDateSelect: PropTypes.bool, 28 | minimumNights: PropTypes.number, 29 | isOutsideRange: PropTypes.func, 30 | isDayBlocked: PropTypes.func, 31 | isDayHighlighted: PropTypes.func, 32 | daysViolatingMinNightsCanBeClicked: PropTypes.bool, 33 | 34 | // DayPicker props 35 | enableOutsideDays: PropTypes.bool, 36 | numberOfMonths: PropTypes.number, 37 | orientation: ScrollableOrientationShape, 38 | verticalHeight: PropTypes.number, 39 | withPortal: PropTypes.bool, 40 | initialVisibleMonth: PropTypes.func, 41 | renderCalendarInfo: PropTypes.func, 42 | renderMonthElement: PropTypes.func, 43 | renderMonthText: PropTypes.func, 44 | 45 | navPrev: PropTypes.node, 46 | navNext: PropTypes.node, 47 | renderNavPrevButton: PropTypes.func, 48 | renderNavNextButton: PropTypes.func, 49 | 50 | onPrevMonthClick: PropTypes.func, 51 | onNextMonthClick: PropTypes.func, 52 | onOutsideClick: PropTypes.func, 53 | renderCalendarDay: PropTypes.func, 54 | renderDayContents: PropTypes.func, 55 | renderKeyboardShortcutsButton: PropTypes.func, 56 | renderKeyboardShortcutsPanel: PropTypes.func, 57 | 58 | // i18n 59 | monthFormat: PropTypes.string, 60 | 61 | isRTL: PropTypes.bool, 62 | }); 63 | 64 | const defaultProps = { 65 | // example props for the demo 66 | autoFocusEndDate: false, 67 | initialStartDate: null, 68 | initialEndDate: null, 69 | startDateOffset: undefined, 70 | endDateOffset: undefined, 71 | showInputs: false, 72 | minDate: null, 73 | maxDate: null, 74 | 75 | // day presentation and interaction related props 76 | renderCalendarDay: undefined, 77 | renderDayContents: null, 78 | minimumNights: 1, 79 | isDayBlocked: () => false, 80 | isOutsideRange: day => !isInclusivelyAfterDay(day, moment()), 81 | isDayHighlighted: () => false, 82 | enableOutsideDays: false, 83 | daysViolatingMinNightsCanBeClicked: false, 84 | 85 | // calendar presentation and interaction related props 86 | orientation: HORIZONTAL_ORIENTATION, 87 | verticalHeight: undefined, 88 | withPortal: false, 89 | initialVisibleMonth: null, 90 | numberOfMonths: 2, 91 | onOutsideClick() {}, 92 | keepOpenOnDateSelect: false, 93 | renderCalendarInfo: null, 94 | isRTL: false, 95 | renderMonthText: null, 96 | renderMonthElement: null, 97 | renderKeyboardShortcutsButton: undefined, 98 | renderKeyboardShortcutsPanel: undefined, 99 | 100 | // navigation related props 101 | navPrev: null, 102 | navNext: null, 103 | renderNavPrevButton: null, 104 | renderNavNextButton: null, 105 | onPrevMonthClick() {}, 106 | onNextMonthClick() {}, 107 | 108 | // internationalization 109 | monthFormat: 'MMMM YYYY', 110 | }; 111 | 112 | class DayPickerRangeControllerWrapper extends React.Component { 113 | constructor(props) { 114 | super(props); 115 | 116 | this.state = { 117 | errorMessage: null, 118 | focusedInput: props.autoFocusEndDate ? END_DATE : START_DATE, 119 | startDate: props.initialStartDate, 120 | endDate: props.initialEndDate, 121 | }; 122 | 123 | this.onDatesChange = this.onDatesChange.bind(this); 124 | this.onFocusChange = this.onFocusChange.bind(this); 125 | } 126 | 127 | onDatesChange({ startDate, endDate }) { 128 | const { daysViolatingMinNightsCanBeClicked, minimumNights } = this.props; 129 | let doesNotMeetMinNights = false; 130 | if (daysViolatingMinNightsCanBeClicked && startDate && endDate) { 131 | const dayDiff = endDate.diff(startDate.clone().startOf('day').hour(12), 'days'); 132 | doesNotMeetMinNights = dayDiff < minimumNights && dayDiff >= 0; 133 | } 134 | this.setState({ 135 | startDate, 136 | endDate: doesNotMeetMinNights ? null : endDate, 137 | errorMessage: doesNotMeetMinNights 138 | ? 'That day does not meet the minimum nights requirement' 139 | : null, 140 | }); 141 | } 142 | 143 | onFocusChange(focusedInput) { 144 | this.setState({ 145 | // Force the focusedInput to always be truthy so that dates are always selectable 146 | focusedInput: !focusedInput ? START_DATE : focusedInput, 147 | }); 148 | } 149 | 150 | render() { 151 | const { renderCalendarInfo: renderCalendarInfoProp, showInputs } = this.props; 152 | const { 153 | errorMessage, 154 | focusedInput, 155 | startDate, 156 | endDate, 157 | } = this.state; 158 | 159 | const props = omit(this.props, [ 160 | 'autoFocus', 161 | 'autoFocusEndDate', 162 | 'initialStartDate', 163 | 'initialEndDate', 164 | 'showInputs', 165 | ]); 166 | 167 | const startDateString = startDate && startDate.format('YYYY-MM-DD'); 168 | const endDateString = endDate && endDate.format('YYYY-MM-DD'); 169 | const renderCalendarInfo = errorMessage ? () => <div>{errorMessage}</div> : renderCalendarInfoProp; 170 | 171 | return ( 172 | <div style={{ height: '100%' }}> 173 | {showInputs && ( 174 | <div style={{ marginBottom: 16 }}> 175 | <input type="text" name="start date" value={startDateString} readOnly /> 176 | <input type="text" name="end date" value={endDateString} readOnly /> 177 | </div> 178 | )} 179 | 180 | <DayPickerRangeController 181 | {...props} 182 | onDatesChange={this.onDatesChange} 183 | onFocusChange={this.onFocusChange} 184 | focusedInput={focusedInput} 185 | startDate={startDate} 186 | endDate={endDate} 187 | renderCalendarInfo={renderCalendarInfo} 188 | /> 189 | </div> 190 | ); 191 | } 192 | } 193 | 194 | DayPickerRangeControllerWrapper.propTypes = propTypes; 195 | DayPickerRangeControllerWrapper.defaultProps = defaultProps; 196 | 197 | export default DayPickerRangeControllerWrapper; 198 | -------------------------------------------------------------------------------- /examples/DayPickerSingleDateControllerWrapper.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unused-prop-types */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import momentPropTypes from 'react-moment-proptypes'; 5 | import { forbidExtraProps } from 'airbnb-prop-types'; 6 | import moment from 'moment'; 7 | import omit from 'lodash/omit'; 8 | 9 | import DayPickerSingleDateController from '../src/components/DayPickerSingleDateController'; 10 | 11 | import ScrollableOrientationShape from '../src/shapes/ScrollableOrientationShape'; 12 | 13 | import { HORIZONTAL_ORIENTATION } from '../src/constants'; 14 | import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; 15 | 16 | const propTypes = forbidExtraProps({ 17 | // example props for the demo 18 | autoFocus: PropTypes.bool, 19 | initialDate: momentPropTypes.momentObj, 20 | showInput: PropTypes.bool, 21 | 22 | allowUnselect: PropTypes.bool, 23 | keepOpenOnDateSelect: PropTypes.bool, 24 | isOutsideRange: PropTypes.func, 25 | isDayBlocked: PropTypes.func, 26 | isDayHighlighted: PropTypes.func, 27 | 28 | // DayPicker props 29 | enableOutsideDays: PropTypes.bool, 30 | numberOfMonths: PropTypes.number, 31 | orientation: ScrollableOrientationShape, 32 | withPortal: PropTypes.bool, 33 | initialVisibleMonth: PropTypes.func, 34 | renderCalendarInfo: PropTypes.func, 35 | 36 | navPrev: PropTypes.node, 37 | navNext: PropTypes.node, 38 | renderNavPrevButton: PropTypes.func, 39 | renderNavNextButton: PropTypes.func, 40 | 41 | onPrevMonthClick: PropTypes.func, 42 | onNextMonthClick: PropTypes.func, 43 | onOutsideClick: PropTypes.func, 44 | renderCalendarDay: PropTypes.func, 45 | renderDayContents: PropTypes.func, 46 | 47 | // i18n 48 | monthFormat: PropTypes.string, 49 | 50 | isRTL: PropTypes.bool, 51 | }); 52 | 53 | const defaultProps = { 54 | // example props for the demo 55 | autoFocus: false, 56 | initialDate: null, 57 | showInput: false, 58 | 59 | // day presentation and interaction related props 60 | allowUnselect: false, 61 | renderCalendarDay: undefined, 62 | renderDayContents: null, 63 | isDayBlocked: () => false, 64 | isOutsideRange: day => !isInclusivelyAfterDay(day, moment()), 65 | isDayHighlighted: () => false, 66 | enableOutsideDays: false, 67 | 68 | // calendar presentation and interaction related props 69 | orientation: HORIZONTAL_ORIENTATION, 70 | withPortal: false, 71 | initialVisibleMonth: null, 72 | numberOfMonths: 2, 73 | onOutsideClick() {}, 74 | keepOpenOnDateSelect: false, 75 | renderCalendarInfo: null, 76 | isRTL: false, 77 | 78 | // navigation related props 79 | navPrev: null, 80 | navNext: null, 81 | renderNavPrevButton: null, 82 | renderNavNextButton: null, 83 | onPrevMonthClick() {}, 84 | onNextMonthClick() {}, 85 | 86 | // internationalization 87 | monthFormat: 'MMMM YYYY', 88 | }; 89 | 90 | class DayPickerSingleDateControllerWrapper extends React.Component { 91 | constructor(props) { 92 | super(props); 93 | 94 | this.state = { 95 | focused: true, 96 | date: props.initialDate, 97 | }; 98 | 99 | this.onDateChange = this.onDateChange.bind(this); 100 | this.onFocusChange = this.onFocusChange.bind(this); 101 | } 102 | 103 | onDateChange(date) { 104 | this.setState({ date }); 105 | } 106 | 107 | onFocusChange() { 108 | // Force the focused states to always be truthy so that date is always selectable 109 | this.setState({ focused: true }); 110 | } 111 | 112 | render() { 113 | const { showInput } = this.props; 114 | const { focused, date } = this.state; 115 | 116 | const props = omit(this.props, [ 117 | 'autoFocus', 118 | 'initialDate', 119 | 'showInput', 120 | ]); 121 | 122 | const dateString = date && date.format('YYYY-MM-DD'); 123 | 124 | return ( 125 | <div> 126 | {showInput && ( 127 | <div style={{ marginBottom: 16 }}> 128 | <input type="text" name="start date" value={dateString || ''} readOnly /> 129 | </div> 130 | )} 131 | 132 | <DayPickerSingleDateController 133 | {...props} 134 | onDateChange={this.onDateChange} 135 | onFocusChange={this.onFocusChange} 136 | focused={focused} 137 | date={date} 138 | /> 139 | </div> 140 | ); 141 | } 142 | } 143 | 144 | DayPickerSingleDateControllerWrapper.propTypes = propTypes; 145 | DayPickerSingleDateControllerWrapper.defaultProps = defaultProps; 146 | 147 | export default DayPickerSingleDateControllerWrapper; 148 | -------------------------------------------------------------------------------- /examples/PresetDateRangePicker.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import momentPropTypes from 'react-moment-proptypes'; 4 | import moment from 'moment'; 5 | import omit from 'lodash/omit'; 6 | 7 | import { withStyles, withStylesPropTypes, css } from 'react-with-styles'; 8 | 9 | import DateRangePicker from '../src/components/DateRangePicker'; 10 | 11 | import { DateRangePickerPhrases } from '../src/defaultPhrases'; 12 | import DateRangePickerShape from '../src/shapes/DateRangePickerShape'; 13 | import { START_DATE, END_DATE, HORIZONTAL_ORIENTATION, ANCHOR_LEFT } from '../src/constants'; 14 | import isSameDay from '../src/utils/isSameDay'; 15 | 16 | const propTypes = { 17 | ...withStylesPropTypes, 18 | 19 | // example props for the demo 20 | autoFocus: PropTypes.bool, 21 | autoFocusEndDate: PropTypes.bool, 22 | initialStartDate: momentPropTypes.momentObj, 23 | initialEndDate: momentPropTypes.momentObj, 24 | presets: PropTypes.arrayOf(PropTypes.shape({ 25 | text: PropTypes.string, 26 | start: momentPropTypes.momentObj, 27 | end: momentPropTypes.momentObj, 28 | })), 29 | 30 | ...omit(DateRangePickerShape, [ 31 | 'startDate', 32 | 'endDate', 33 | 'onDatesChange', 34 | 'focusedInput', 35 | 'onFocusChange', 36 | ]), 37 | }; 38 | 39 | const defaultProps = { 40 | // example props for the demo 41 | autoFocus: false, 42 | autoFocusEndDate: false, 43 | initialStartDate: null, 44 | initialEndDate: null, 45 | presets: [], 46 | 47 | // input related props 48 | startDateId: START_DATE, 49 | startDatePlaceholderText: 'Start Date', 50 | endDateId: END_DATE, 51 | endDatePlaceholderText: 'End Date', 52 | disabled: false, 53 | required: false, 54 | screenReaderInputMessage: '', 55 | showClearDates: false, 56 | showDefaultInputIcon: false, 57 | customInputIcon: null, 58 | customArrowIcon: null, 59 | customCloseIcon: null, 60 | 61 | // calendar presentation and interaction related props 62 | renderMonthText: null, 63 | orientation: HORIZONTAL_ORIENTATION, 64 | anchorDirection: ANCHOR_LEFT, 65 | horizontalMargin: 0, 66 | withPortal: false, 67 | withFullScreenPortal: false, 68 | initialVisibleMonth: null, 69 | numberOfMonths: 2, 70 | keepOpenOnDateSelect: false, 71 | reopenPickerOnClearDates: false, 72 | isRTL: false, 73 | 74 | // navigation related props 75 | navPrev: null, 76 | navNext: null, 77 | onPrevMonthClick() {}, 78 | onNextMonthClick() {}, 79 | onClose() {}, 80 | 81 | // day presentation and interaction related props 82 | renderDayContents: null, 83 | minimumNights: 0, 84 | enableOutsideDays: false, 85 | isDayBlocked: () => false, 86 | isOutsideRange: day => false, 87 | isDayHighlighted: () => false, 88 | 89 | // internationalization 90 | displayFormat: () => moment.localeData().longDateFormat('L'), 91 | monthFormat: 'MMMM YYYY', 92 | phrases: DateRangePickerPhrases, 93 | }; 94 | 95 | class DateRangePickerWrapper extends React.Component { 96 | constructor(props) { 97 | super(props); 98 | 99 | let focusedInput = null; 100 | if (props.autoFocus) { 101 | focusedInput = START_DATE; 102 | } else if (props.autoFocusEndDate) { 103 | focusedInput = END_DATE; 104 | } 105 | 106 | this.state = { 107 | focusedInput, 108 | startDate: props.initialStartDate, 109 | endDate: props.initialEndDate, 110 | }; 111 | 112 | this.onDatesChange = this.onDatesChange.bind(this); 113 | this.onFocusChange = this.onFocusChange.bind(this); 114 | this.renderDatePresets = this.renderDatePresets.bind(this); 115 | } 116 | 117 | onDatesChange({ startDate, endDate }) { 118 | this.setState({ startDate, endDate }); 119 | } 120 | 121 | onFocusChange(focusedInput) { 122 | this.setState({ focusedInput }); 123 | } 124 | 125 | renderDatePresets() { 126 | const { presets, styles } = this.props; 127 | const { startDate, endDate } = this.state; 128 | 129 | return ( 130 | <div {...css(styles.PresetDateRangePicker_panel)}> 131 | {presets.map(({ text, start, end }) => { 132 | const isSelected = isSameDay(start, startDate) && isSameDay(end, endDate); 133 | return ( 134 | <button 135 | key={text} 136 | {...css( 137 | styles.PresetDateRangePicker_button, 138 | isSelected && styles.PresetDateRangePicker_button__selected, 139 | )} 140 | type="button" 141 | onClick={() => this.onDatesChange({ startDate: start, endDate: end })} 142 | > 143 | {text} 144 | </button> 145 | ); 146 | })} 147 | </div> 148 | ); 149 | } 150 | 151 | render() { 152 | const { focusedInput, startDate, endDate } = this.state; 153 | 154 | // autoFocus, autoFocusEndDate, initialStartDate and initialEndDate are helper props for the 155 | // example wrapper but are not props on the SingleDatePicker itself and 156 | // thus, have to be omitted. 157 | const props = omit(this.props, [ 158 | 'autoFocus', 159 | 'autoFocusEndDate', 160 | 'initialStartDate', 161 | 'initialEndDate', 162 | 'presets', 163 | ]); 164 | 165 | return ( 166 | <div> 167 | <DateRangePicker 168 | {...props} 169 | renderCalendarInfo={this.renderDatePresets} 170 | onDatesChange={this.onDatesChange} 171 | onFocusChange={this.onFocusChange} 172 | focusedInput={focusedInput} 173 | startDate={startDate} 174 | endDate={endDate} 175 | /> 176 | </div> 177 | ); 178 | } 179 | } 180 | 181 | DateRangePickerWrapper.propTypes = propTypes; 182 | DateRangePickerWrapper.defaultProps = defaultProps; 183 | 184 | export default withStyles(({ reactDates: { color } }) => ({ 185 | PresetDateRangePicker_panel: { 186 | padding: '0 22px 11px 22px', 187 | }, 188 | 189 | PresetDateRangePicker_button: { 190 | position: 'relative', 191 | height: '100%', 192 | textAlign: 'center', 193 | background: 'none', 194 | border: `2px solid ${color.core.primary}`, 195 | color: color.core.primary, 196 | padding: '4px 12px', 197 | marginRight: 8, 198 | font: 'inherit', 199 | fontWeight: 700, 200 | lineHeight: 'normal', 201 | overflow: 'visible', 202 | boxSizing: 'border-box', 203 | cursor: 'pointer', 204 | 205 | ':active': { 206 | outline: 0, 207 | }, 208 | }, 209 | 210 | PresetDateRangePicker_button__selected: { 211 | color: color.core.white, 212 | background: color.core.primary, 213 | }, 214 | }))(DateRangePickerWrapper); 215 | -------------------------------------------------------------------------------- /examples/SingleDatePickerWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import momentPropTypes from 'react-moment-proptypes'; 4 | import moment from 'moment'; 5 | import omit from 'lodash/omit'; 6 | 7 | import SingleDatePicker from '../src/components/SingleDatePicker'; 8 | 9 | import { SingleDatePickerPhrases } from '../src/defaultPhrases'; 10 | import SingleDatePickerShape from '../src/shapes/SingleDatePickerShape'; 11 | import { HORIZONTAL_ORIENTATION, ANCHOR_LEFT, OPEN_DOWN } from '../src/constants'; 12 | import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; 13 | 14 | const propTypes = { 15 | // example props for the demo 16 | autoFocus: PropTypes.bool, 17 | initialDate: momentPropTypes.momentObj, 18 | 19 | ...omit(SingleDatePickerShape, [ 20 | 'date', 21 | 'onDateChange', 22 | 'focused', 23 | 'onFocusChange', 24 | ]), 25 | }; 26 | 27 | const defaultProps = { 28 | // example props for the demo 29 | autoFocus: false, 30 | initialDate: null, 31 | 32 | // input related props 33 | id: 'date', 34 | placeholder: 'Date', 35 | disabled: false, 36 | required: false, 37 | screenReaderInputMessage: '', 38 | showClearDate: false, 39 | showDefaultInputIcon: false, 40 | customInputIcon: null, 41 | block: false, 42 | small: false, 43 | regular: false, 44 | verticalSpacing: undefined, 45 | keepFocusOnInput: false, 46 | autoComplete: 'off', 47 | 48 | // calendar presentation and interaction related props 49 | renderMonthText: null, 50 | orientation: HORIZONTAL_ORIENTATION, 51 | anchorDirection: ANCHOR_LEFT, 52 | horizontalMargin: 0, 53 | withPortal: false, 54 | withFullScreenPortal: false, 55 | initialVisibleMonth: null, 56 | numberOfMonths: 2, 57 | keepOpenOnDateSelect: false, 58 | reopenPickerOnClearDate: false, 59 | isRTL: false, 60 | openDirection: OPEN_DOWN, 61 | 62 | // navigation related props 63 | navPrev: null, 64 | navNext: null, 65 | onPrevMonthClick() {}, 66 | onNextMonthClick() {}, 67 | onClose() {}, 68 | 69 | // day presentation and interaction related props 70 | renderCalendarDay: undefined, 71 | renderDayContents: null, 72 | enableOutsideDays: false, 73 | isDayBlocked: () => false, 74 | isOutsideRange: day => !isInclusivelyAfterDay(day, moment()), 75 | isDayHighlighted: () => {}, 76 | 77 | // internationalization props 78 | displayFormat: () => moment.localeData().longDateFormat('L'), 79 | monthFormat: 'MMMM YYYY', 80 | phrases: SingleDatePickerPhrases, 81 | }; 82 | 83 | class SingleDatePickerWrapper extends React.Component { 84 | constructor(props) { 85 | super(props); 86 | this.state = { 87 | focused: props.autoFocus, 88 | date: props.initialDate, 89 | }; 90 | 91 | this.onDateChange = this.onDateChange.bind(this); 92 | this.onFocusChange = this.onFocusChange.bind(this); 93 | } 94 | 95 | onDateChange(date) { 96 | this.setState({ date }); 97 | } 98 | 99 | onFocusChange({ focused }) { 100 | this.setState({ focused }); 101 | } 102 | 103 | render() { 104 | const { focused, date } = this.state; 105 | 106 | // autoFocus and initialDate are helper props for the example wrapper but are not 107 | // props on the SingleDatePicker itself and thus, have to be omitted. 108 | const props = omit(this.props, [ 109 | 'autoFocus', 110 | 'initialDate', 111 | ]); 112 | 113 | return ( 114 | <SingleDatePicker 115 | {...props} 116 | id="date_input" 117 | date={date} 118 | focused={focused} 119 | onDateChange={this.onDateChange} 120 | onFocusChange={this.onFocusChange} 121 | /> 122 | ); 123 | } 124 | } 125 | 126 | SingleDatePickerWrapper.propTypes = propTypes; 127 | SingleDatePickerWrapper.defaultProps = defaultProps; 128 | 129 | export default SingleDatePickerWrapper; 130 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | module.exports = require('./lib'); 3 | -------------------------------------------------------------------------------- /initialize.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | require('./lib/initialize'); 3 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint no-param-reassign:0, import/no-extraneous-dependencies:0 */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = (config) => { 6 | config.set({ 7 | basePath: '', 8 | 9 | frameworks: ['mocha', 'sinon', 'chai'], 10 | 11 | files: ['test/browser-main.js'], 12 | 13 | webpack: { 14 | mode: 'development', 15 | externals: { 16 | sinon: true, 17 | }, 18 | plugins: [ 19 | // https://github.com/cheeriojs/cheerio/issues/836 20 | new webpack.NormalModuleReplacementPlugin(/^\.\/package$/, (result) => { 21 | if (/cheerio/.test(result.context)) { 22 | result.request = './package.json'; 23 | } 24 | }), 25 | ], 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.jsx?$/, 30 | loader: 'babel-loader', 31 | include: [ 32 | path.join(__dirname, 'src'), 33 | path.join(__dirname, 'test'), 34 | require.resolve('airbnb-js-shims'), 35 | ], 36 | options: { 37 | presets: [ 38 | // setting modules to false so it does not transform twice 39 | ['airbnb', { modules: false }], 40 | // transform to cjs so sinon can stub properly 41 | ['@babel/preset-env', { modules: 'cjs' }], 42 | ], 43 | }, 44 | }, 45 | 46 | // Inject the Airbnb shims into the bundle 47 | { test: /test\/_helpers/, loader: 'imports-loader?shims=airbnb-js-shims' }, 48 | ], 49 | }, 50 | resolve: { 51 | extensions: ['.js', '.jsx'], 52 | }, 53 | }, 54 | 55 | webpackMiddleware: { 56 | progress: false, 57 | stats: false, 58 | debug: false, 59 | quiet: true, 60 | }, 61 | 62 | preprocessors: { 63 | 'test/**/*': ['webpack'], 64 | }, 65 | 66 | reporters: ['progress'], 67 | 68 | port: 9876, 69 | 70 | colors: true, 71 | 72 | logLevel: config.LOG_INFO, 73 | 74 | autoWatch: false, 75 | 76 | browsers: ['Firefox'], 77 | 78 | singleRun: true, 79 | 80 | concurrency: Infinity, 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dates", 3 | "version": "21.8.0", 4 | "description": "A responsive and accessible date range picker component built with React", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "npm run clean", 8 | "build": "npm run build:cjs && npm run build:esm && npm run build:css -- --optimize", 9 | "postbuild": "npm run build:test", 10 | "build:cjs": "BABEL_ENV=cjs babel src/ -d lib/", 11 | "build:esm": "BABEL_ENV=esm babel src/ -d esm/", 12 | "build:test": "BABEL_ENV=test babel test/ -d test-build/", 13 | "prebuild:css": "rimraf lib/css && mkdirp lib/css", 14 | "build:css": "node scripts/buildCSS.js", 15 | "clean": "rimraf lib esm test-build", 16 | "lint": "eslint --ext .js,.jsx src test", 17 | "mocha": "mocha ./test-build/_helpers", 18 | "storybook:uninstall": "npm uninstall --no-save @storybook/react && rimraf node_modules/@storybook node_modules/react-modal node_modules/react-dom-factories", 19 | "react": "NPM_CONFIG_LEGACY_PEER_DEPS=true enzyme-adapter-react-install 16", 20 | "pretest": "npm run --silent lint", 21 | "tests-only": "cross-env NODE_ENV=test nyc npm run mocha --silent", 22 | "pretests-karma": "npm run react", 23 | "tests-karma": "karma start", 24 | "test": "npm run react && npm run build && npm run build:test && npm run tests-only", 25 | "storybook": "start-storybook -p 6006", 26 | "storybook:css": "npm run build:css && start-storybook -p 6006 -c .storybook-css", 27 | "tag": "git tag v$npm_package_version", 28 | "gh-pages:clean": "rimraf _gh-pages", 29 | "gh-pages:build": "$(npm bin)/build-storybook -o _gh-pages", 30 | "gh-pages:publish": "$(npm bin)/git-directory-deploy --directory _gh-pages", 31 | "gh-pages": "npm run gh-pages:clean && npm run gh-pages:build && npm run gh-pages:publish", 32 | "version:patch": "npm --no-git-tag-version version patch", 33 | "version:minor": "npm --no-git-tag-version version minor", 34 | "version:major": "npm --no-git-tag-version version major", 35 | "preversion": "npm run test && npm run check-changelog && npm run check-only-changelog-changed", 36 | "postversion": "git commit package.json CHANGELOG.md -m \"Version $npm_package_version\" && npm run tag && git push && git push --tags && npm publish --registry=https://registry.npmjs.org/", 37 | "prepublish": "in-publish && safe-publish-latest && npm run build || not-in-publish", 38 | "postpublish": "[ \"${npm_config_tag:-latest}\" != latest ] || npm run gh-pages", 39 | "check-changelog": "expr $(git status --porcelain 2>/dev/null| grep \"^\\s*M.*CHANGELOG.md\" | wc -l) >/dev/null || (echo 'Please edit CHANGELOG.md' && exit 1)", 40 | "check-only-changelog-changed": "(expr $(git status --porcelain 2>/dev/null| grep -v \"CHANGELOG.md\" | wc -l) >/dev/null && echo 'Only CHANGELOG.md may have uncommitted changes' && exit 1) || exit 0" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/react-dates/react-dates.git" 45 | }, 46 | "author": "Maja Wichrowska <maja.wichrowska@airbnb.com>", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/react-dates/react-dates/issues" 50 | }, 51 | "homepage": "https://github.com/react-dates/react-dates#readme", 52 | "devDependencies": { 53 | "@babel/cli": "^7.16.8", 54 | "@babel/core": "^7.16.12", 55 | "@babel/eslint-parser": "^7.16.5", 56 | "@babel/register": "^7.16.9", 57 | "@babel/runtime": "^7.16.7", 58 | "@storybook/addon-actions": "^5.3.21", 59 | "@storybook/addon-info": "^5.3.21", 60 | "@storybook/addon-links": "^5.3.21", 61 | "@storybook/addon-options": "^5.3.21", 62 | "@storybook/addons": "^5.3.21", 63 | "@storybook/react": "^5.3.21", 64 | "@welldone-software/why-did-you-render": "^3.6.0", 65 | "airbnb-js-shims": "^2.2.1", 66 | "aphrodite": "^2.4.0", 67 | "babel-loader": "^8.2.2", 68 | "babel-plugin-import-path-replace": "^0.1.0", 69 | "babel-plugin-inline-react-svg": "^1.1.2", 70 | "babel-plugin-inline-svg": "^1.2.0", 71 | "babel-plugin-istanbul": "^5.2.0", 72 | "babel-plugin-transform-replace-object-assign": "^2.0.0", 73 | "babel-preset-airbnb": "^4.5.0", 74 | "chai": "^4.2.0", 75 | "cheerio": "=1.0.0-rc.3", 76 | "clean-css": "^4.2.3", 77 | "cross-env": "^5.2.1", 78 | "enzyme": "^3.11.0", 79 | "enzyme-adapter-react-helper": "^1.3.9", 80 | "eslint": "^8.7.0", 81 | "eslint-config-airbnb": "^19.0.4", 82 | "eslint-plugin-import": "^2.25.4", 83 | "eslint-plugin-jsx-a11y": "^6.5.1", 84 | "eslint-plugin-react": "^7.28.0", 85 | "eslint-plugin-react-hooks": "^4.3.0", 86 | "eslint-plugin-react-with-styles": "^2.4.0", 87 | "git-directory-deploy": "^1.5.1", 88 | "imports-loader": "^0.8.0", 89 | "in-publish": "^2.0.1", 90 | "karma": "^6.3.11", 91 | "karma-chai": "^0.1.0", 92 | "karma-firefox-launcher": "^1.2.0", 93 | "karma-mocha": "^2.0.1", 94 | "karma-sinon": "^1.0.5", 95 | "karma-webpack": "^4.0.2", 96 | "mkdirp": "^0.5.5", 97 | "mocha": "^3.5.3", 98 | "mocha-wrap": "^2.1.2", 99 | "moment": "^2.29.1", 100 | "moment-jalaali": "^0.7.4", 101 | "nyc": "^10.3.2", 102 | "raw-loader": "^0.5.1", 103 | "react": "^0.14 || ^15.5.4 || ^16.1.1", 104 | "react-dom": "^0.14 || ^15.5.4 || ^16.1.1", 105 | "react-with-styles-interface-aphrodite": "^6.0.1", 106 | "react-with-styles-interface-css-compiler": "^2.2.0", 107 | "rimraf": "^2.7.1", 108 | "safe-publish-latest": "^1.1.4", 109 | "sass-loader": "^7.3.1", 110 | "sinon": "^7.5.0", 111 | "sinon-sandbox": "^2.0.6", 112 | "style-loader": "^0.20.3", 113 | "typescript": "*", 114 | "webpack": "^4.46.0" 115 | }, 116 | "dependencies": { 117 | "airbnb-prop-types": "^2.16.0", 118 | "color2k": "~1.1.1", 119 | "consolidated-events": "^1.1.1 || ^2.0.0", 120 | "enzyme-shallow-equal": "^1.0.4", 121 | "is-touch-device": "^1.0.1", 122 | "lodash": "^4.1.1", 123 | "object.assign": "^4.1.2", 124 | "object.values": "^1.1.5", 125 | "prop-types": "^15.7.2", 126 | "raf": "^3.4.1", 127 | "react-moment-proptypes": "^1.8.1", 128 | "react-outside-click-handler": "^1.3.0", 129 | "react-portal": "^4.2.1", 130 | "react-with-direction": "^1.4.0", 131 | "react-with-styles": "~4.1.0", 132 | "react-with-styles-interface-css": "^6.0.0" 133 | }, 134 | "peerDependencies": { 135 | "@babel/runtime": "^7.0.0", 136 | "moment": "^2.18.1", 137 | "react": "^0.14 || ^15.5.4 || ^16.1.1", 138 | "react-dom": "^0.14 || ^15.5.4 || ^16.1.1" 139 | }, 140 | "engines": { 141 | "node": ">=0.10" 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /react-dates-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-dates/react-dates/b7bad38dcff024d374ee98f972b55b3de9e61289/react-dates-demo.gif -------------------------------------------------------------------------------- /scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": [2, { 4 | "devDependencies": true 5 | }], 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /scripts/buildCSS.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const CleanCSS = require('clean-css'); 5 | 6 | const compileCSS = require('react-with-styles-interface-css-compiler'); 7 | 8 | const registerMaxSpecificity = require('react-with-styles-interface-css/dist/utils/registerMaxSpecificity').default; 9 | const registerCSSInterfaceWithDefaultTheme = require('../src/utils/registerCSSInterfaceWithDefaultTheme').default; 10 | 11 | console.error = msg => { throw new SyntaxError(msg); }; 12 | console.warn = msg => { throw new SyntaxError(msg); }; 13 | 14 | const args = process.argv.slice(2); 15 | const optimizeForProduction = args.includes('-o') || args.includes('--optimize'); 16 | 17 | registerMaxSpecificity(0); 18 | registerCSSInterfaceWithDefaultTheme(); 19 | 20 | const path = './scripts/renderAllComponents.jsx'; 21 | const CSS = compileCSS(path); 22 | 23 | if (CSS === '') { 24 | throw new Error('Failed to compile CSS'); 25 | } else { 26 | console.log('CSS compilation complete.'); 27 | } 28 | 29 | const format = new CleanCSS({ 30 | level: optimizeForProduction ? 2 : 0, 31 | format: 'beautify', 32 | inline: ['none'], 33 | }); 34 | const { styles: formattedCSS } = format.minify(CSS); 35 | 36 | const outputFilePath = optimizeForProduction ? './lib/css/_datepicker.css' : './css/styles.css'; 37 | fs.writeFileSync(outputFilePath, formattedCSS, 'utf8'); 38 | -------------------------------------------------------------------------------- /scripts/pure-component-fallback.js: -------------------------------------------------------------------------------- 1 | module.exports = function pureComponentFallback({ types: t, template }) { 2 | const buildPureOrNormalSuperclass = () => t.LogicalExpression('||', 3 | t.memberExpression(t.identifier('React'), t.identifier('PureComponent')), 4 | t.memberExpression(t.identifier('React'), t.identifier('Component'))); 5 | 6 | const buildShouldComponentUpdate = () => { 7 | const method = t.classMethod( 8 | 'method', 9 | t.logicalExpression( 10 | '&&', 11 | t.unaryExpression('!', t.memberExpression(t.identifier('React'), t.identifier('PureComponent'))), 12 | t.stringLiteral('shouldComponentUpdate') 13 | ), 14 | [t.identifier('nextProps'), t.identifier('nextState')], 15 | t.blockStatement([template('return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);')()]), 16 | true 17 | ); 18 | return method; 19 | }; 20 | 21 | function superclassIsPureComponent(path) { 22 | const superclass = path.get('superClass'); 23 | // check for PureComponent 24 | if (t.isIdentifier(superclass)) { 25 | return superclass.node.name === 'PureComponent'; 26 | } 27 | 28 | if (t.isMemberExpression(superclass)) { 29 | // Check for React.PureComponent 30 | return superclass.get('object').node.name === 'React' 31 | && superclass.get('property').node.name === 'PureComponent'; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | return { 38 | visitor: { 39 | Program: { 40 | exit({ node }, { file }) { 41 | if (file.get('addShallowEqualImport')) { 42 | const shallowEqualImportDeclaration = t.importDeclaration([ 43 | t.importDefaultSpecifier(t.identifier('shallowEqual')), 44 | ], t.stringLiteral('enzyme-shallow-equal')); 45 | node.body.unshift(shallowEqualImportDeclaration); 46 | } 47 | }, 48 | }, 49 | 50 | ClassDeclaration(path, { file }) { 51 | if (superclassIsPureComponent(path)) { 52 | const superclassPath = path.get('superClass'); 53 | 54 | // Replace the superclass 55 | superclassPath.replaceWith(buildPureOrNormalSuperclass()); 56 | 57 | // Only add an SCU if one doesn't already exist 58 | const existingSCU = path.get('body').get('body').find(( 59 | p => p.isClassMethod() && p.get('key').node.name === 'shouldComponentUpdate' 60 | )); 61 | 62 | if (!existingSCU) { 63 | file.set('addShallowEqualImport', true); 64 | path.get('body').unshiftContainer('body', buildShouldComponentUpdate()); 65 | } 66 | } 67 | }, 68 | }, 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /scripts/renderAllComponents.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import DateRangePickerWrapper from '../examples/DateRangePickerWrapper'; 5 | import SingleDatePickerWrapper from '../examples/SingleDatePickerWrapper'; 6 | import PresetDateRangePickerWrapper from '../examples/PresetDateRangePicker'; 7 | 8 | if (!document.getElementById('root')) { 9 | // Make sure the #root element is defined 10 | const root = document.createElement('div'); 11 | root.id = 'root'; 12 | document.body.appendChild(root); 13 | } 14 | 15 | function App() { 16 | return ( 17 | <div> 18 | <DateRangePickerWrapper autoFocus /> 19 | <SingleDatePickerWrapper autoFocus /> 20 | <PresetDateRangePickerWrapper autoFocus /> 21 | </div> 22 | ); 23 | } 24 | 25 | ReactDOM.render(<App />, document.querySelector('#root')); 26 | -------------------------------------------------------------------------------- /src/components/CalendarIcon.jsx: -------------------------------------------------------------------------------- 1 | import CalendarIcon from '../svg/calendar.svg'; 2 | 3 | export default CalendarIcon; 4 | -------------------------------------------------------------------------------- /src/components/CalendarWeek.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { forbidExtraProps } from 'airbnb-prop-types'; 4 | 5 | const propTypes = forbidExtraProps({ 6 | children: PropTypes.node.isRequired, 7 | }); 8 | 9 | export default function CalendarWeek({ children }) { 10 | return ( 11 | <tr> 12 | {children} 13 | </tr> 14 | ); 15 | } 16 | 17 | CalendarWeek.propTypes = propTypes; 18 | -------------------------------------------------------------------------------- /src/components/ChevronDown.jsx: -------------------------------------------------------------------------------- 1 | import ChevronDown from '../svg/chevron-down.svg'; 2 | 3 | export default ChevronDown; 4 | -------------------------------------------------------------------------------- /src/components/ChevronUp.jsx: -------------------------------------------------------------------------------- 1 | import ChevronUp from '../svg/chevron-up.svg'; 2 | 3 | export default ChevronUp; 4 | -------------------------------------------------------------------------------- /src/components/CloseButton.jsx: -------------------------------------------------------------------------------- 1 | import CloseButton from '../svg/close.svg'; 2 | 3 | export default CloseButton; 4 | -------------------------------------------------------------------------------- /src/components/KeyboardShortcutRow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { forbidExtraProps } from 'airbnb-prop-types'; 4 | import { withStyles, withStylesPropTypes } from 'react-with-styles'; 5 | 6 | const propTypes = forbidExtraProps({ 7 | ...withStylesPropTypes, 8 | unicode: PropTypes.string.isRequired, 9 | label: PropTypes.string.isRequired, 10 | action: PropTypes.string.isRequired, 11 | block: PropTypes.bool, 12 | }); 13 | 14 | const defaultProps = { 15 | block: false, 16 | }; 17 | 18 | function KeyboardShortcutRow({ 19 | unicode, 20 | label, 21 | action, 22 | block, 23 | css, 24 | styles, 25 | }) { 26 | return ( 27 | <li 28 | {...css( 29 | styles.KeyboardShortcutRow, 30 | block && styles.KeyboardShortcutRow__block, 31 | )} 32 | > 33 | <div 34 | {...css( 35 | styles.KeyboardShortcutRow_keyContainer, 36 | block && styles.KeyboardShortcutRow_keyContainer__block, 37 | )} 38 | > 39 | <span 40 | {...css(styles.KeyboardShortcutRow_key)} 41 | role="img" 42 | aria-label={`${label},`} // add comma so screen readers will pause before reading action 43 | > 44 | {unicode} 45 | </span> 46 | </div> 47 | 48 | <div {...css(styles.KeyboardShortcutRow_action)}> 49 | {action} 50 | </div> 51 | </li> 52 | ); 53 | } 54 | 55 | KeyboardShortcutRow.propTypes = propTypes; 56 | KeyboardShortcutRow.defaultProps = defaultProps; 57 | 58 | export default withStyles(({ reactDates: { color } }) => ({ 59 | KeyboardShortcutRow: { 60 | listStyle: 'none', 61 | margin: '6px 0', 62 | }, 63 | 64 | KeyboardShortcutRow__block: { 65 | marginBottom: 16, 66 | }, 67 | 68 | KeyboardShortcutRow_keyContainer: { 69 | display: 'inline-block', 70 | whiteSpace: 'nowrap', 71 | textAlign: 'right', // is not handled by isRTL 72 | marginRight: 6, // is not handled by isRTL 73 | }, 74 | 75 | KeyboardShortcutRow_keyContainer__block: { 76 | textAlign: 'left', // is not handled by isRTL 77 | display: 'inline', 78 | }, 79 | 80 | KeyboardShortcutRow_key: { 81 | fontFamily: 'monospace', 82 | fontSize: 12, 83 | textTransform: 'uppercase', 84 | background: color.core.grayLightest, 85 | padding: '2px 6px', 86 | }, 87 | 88 | KeyboardShortcutRow_action: { 89 | display: 'inline', 90 | wordBreak: 'break-word', 91 | marginLeft: 8, // is not handled by isRTL 92 | }, 93 | }), { pureComponent: typeof React.PureComponent !== 'undefined' })(KeyboardShortcutRow); 94 | -------------------------------------------------------------------------------- /src/components/LeftArrow.jsx: -------------------------------------------------------------------------------- 1 | import LeftArrow from '../svg/arrow-left.svg'; 2 | 3 | export default LeftArrow; 4 | -------------------------------------------------------------------------------- /src/components/RightArrow.jsx: -------------------------------------------------------------------------------- 1 | import RightArrow from '../svg/arrow-right.svg'; 2 | 3 | export default RightArrow; 4 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const DISPLAY_FORMAT = 'L'; 2 | export const ISO_FORMAT = 'YYYY-MM-DD'; 3 | export const ISO_MONTH_FORMAT = 'YYYY-MM'; // TODO delete this line of dead code on next breaking change 4 | 5 | export const START_DATE = 'startDate'; 6 | export const END_DATE = 'endDate'; 7 | 8 | export const HORIZONTAL_ORIENTATION = 'horizontal'; 9 | export const VERTICAL_ORIENTATION = 'vertical'; 10 | export const VERTICAL_SCROLLABLE = 'verticalScrollable'; 11 | 12 | export const NAV_POSITION_BOTTOM = 'navPositionBottom'; 13 | export const NAV_POSITION_TOP = 'navPositionTop'; 14 | 15 | export const ICON_BEFORE_POSITION = 'before'; 16 | export const ICON_AFTER_POSITION = 'after'; 17 | 18 | export const INFO_POSITION_TOP = 'top'; 19 | export const INFO_POSITION_BOTTOM = 'bottom'; 20 | export const INFO_POSITION_BEFORE = 'before'; 21 | export const INFO_POSITION_AFTER = 'after'; 22 | 23 | export const ANCHOR_LEFT = 'left'; 24 | export const ANCHOR_RIGHT = 'right'; 25 | 26 | export const OPEN_DOWN = 'down'; 27 | export const OPEN_UP = 'up'; 28 | 29 | export const DAY_SIZE = 39; 30 | export const BLOCKED_MODIFIER = 'blocked'; 31 | export const WEEKDAYS = [0, 1, 2, 3, 4, 5, 6]; 32 | 33 | export const FANG_WIDTH_PX = 20; 34 | export const FANG_HEIGHT_PX = 10; 35 | export const DEFAULT_VERTICAL_SPACING = 22; 36 | 37 | export const MODIFIER_KEY_NAMES = new Set(['Shift', 'Control', 'Alt', 'Meta']); 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as CalendarDay } from './components/CalendarDay'; 2 | export { default as CalendarMonth } from './components/CalendarMonth'; 3 | export { default as CalendarMonthGrid } from './components/CalendarMonthGrid'; 4 | export { default as DateRangePicker } from './components/DateRangePicker'; 5 | export { default as DateRangePickerInput } from './components/DateRangePickerInput'; 6 | export { default as DateRangePickerInputController } from './components/DateRangePickerInputController'; 7 | export { default as DateRangePickerShape } from './shapes/DateRangePickerShape'; 8 | export { default as DayPicker } from './components/DayPicker'; 9 | export { default as DayPickerRangeController } from './components/DayPickerRangeController'; 10 | export { default as DayPickerSingleDateController } from './components/DayPickerSingleDateController'; 11 | export { default as SingleDatePicker } from './components/SingleDatePicker'; 12 | export { default as SingleDatePickerInput } from './components/SingleDatePickerInput'; 13 | export { default as SingleDatePickerShape } from './shapes/SingleDatePickerShape'; 14 | export { default as isInclusivelyAfterDay } from './utils/isInclusivelyAfterDay'; 15 | export { default as isInclusivelyBeforeDay } from './utils/isInclusivelyBeforeDay'; 16 | export { default as isNextDay } from './utils/isNextDay'; 17 | export { default as isSameDay } from './utils/isSameDay'; 18 | export { default as toISODateString } from './utils/toISODateString'; 19 | export { default as toLocalizedDateString } from './utils/toLocalizedDateString'; 20 | export { default as toMomentObject } from './utils/toMomentObject'; 21 | -------------------------------------------------------------------------------- /src/initialize.js: -------------------------------------------------------------------------------- 1 | import registerCSSInterfaceWithDefaultTheme from './utils/registerCSSInterfaceWithDefaultTheme'; 2 | 3 | registerCSSInterfaceWithDefaultTheme(); 4 | -------------------------------------------------------------------------------- /src/shapes/AnchorDirectionShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { 4 | ANCHOR_LEFT, 5 | ANCHOR_RIGHT, 6 | } from '../constants'; 7 | 8 | export default PropTypes.oneOf([ANCHOR_LEFT, ANCHOR_RIGHT]); 9 | -------------------------------------------------------------------------------- /src/shapes/CalendarInfoPositionShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { 4 | INFO_POSITION_TOP, 5 | INFO_POSITION_BOTTOM, 6 | INFO_POSITION_BEFORE, 7 | INFO_POSITION_AFTER, 8 | } from '../constants'; 9 | 10 | export default PropTypes.oneOf([ 11 | INFO_POSITION_TOP, 12 | INFO_POSITION_BOTTOM, 13 | INFO_POSITION_BEFORE, 14 | INFO_POSITION_AFTER, 15 | ]); 16 | -------------------------------------------------------------------------------- /src/shapes/DateRangePickerShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import momentPropTypes from 'react-moment-proptypes'; 3 | import { mutuallyExclusiveProps, nonNegativeInteger } from 'airbnb-prop-types'; 4 | 5 | import { DateRangePickerPhrases } from '../defaultPhrases'; 6 | import getPhrasePropTypes from '../utils/getPhrasePropTypes'; 7 | 8 | import FocusedInputShape from './FocusedInputShape'; 9 | import IconPositionShape from './IconPositionShape'; 10 | import OrientationShape from './OrientationShape'; 11 | import DisabledShape from './DisabledShape'; 12 | import anchorDirectionShape from './AnchorDirectionShape'; 13 | import openDirectionShape from './OpenDirectionShape'; 14 | import DayOfWeekShape from './DayOfWeekShape'; 15 | import CalendarInfoPositionShape from './CalendarInfoPositionShape'; 16 | import NavPositionShape from './NavPositionShape'; 17 | 18 | export default { 19 | // required props for a functional interactive DateRangePicker 20 | startDate: momentPropTypes.momentObj, 21 | endDate: momentPropTypes.momentObj, 22 | onDatesChange: PropTypes.func.isRequired, 23 | 24 | focusedInput: FocusedInputShape, 25 | onFocusChange: PropTypes.func.isRequired, 26 | 27 | onClose: PropTypes.func, 28 | 29 | // input related props 30 | startDateId: PropTypes.string.isRequired, 31 | startDatePlaceholderText: PropTypes.string, 32 | startDateOffset: PropTypes.func, 33 | endDateOffset: PropTypes.func, 34 | endDateId: PropTypes.string.isRequired, 35 | endDatePlaceholderText: PropTypes.string, 36 | startDateAriaLabel: PropTypes.string, 37 | endDateAriaLabel: PropTypes.string, 38 | startDateTitleText: PropTypes.string, 39 | endDateTitleText: PropTypes.string, 40 | disabled: DisabledShape, 41 | required: PropTypes.bool, 42 | readOnly: PropTypes.bool, 43 | screenReaderInputMessage: PropTypes.string, 44 | showClearDates: PropTypes.bool, 45 | showDefaultInputIcon: PropTypes.bool, 46 | inputIconPosition: IconPositionShape, 47 | customInputIcon: PropTypes.node, 48 | customArrowIcon: PropTypes.node, 49 | customCloseIcon: PropTypes.node, 50 | noBorder: PropTypes.bool, 51 | block: PropTypes.bool, 52 | small: PropTypes.bool, 53 | regular: PropTypes.bool, 54 | keepFocusOnInput: PropTypes.bool, 55 | autoComplete: PropTypes.string, 56 | 57 | // calendar presentation and interaction related props 58 | renderMonthText: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'), 59 | renderMonthElement: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'), 60 | renderWeekHeaderElement: PropTypes.func, 61 | orientation: OrientationShape, 62 | anchorDirection: anchorDirectionShape, 63 | openDirection: openDirectionShape, 64 | horizontalMargin: PropTypes.number, 65 | withPortal: PropTypes.bool, 66 | withFullScreenPortal: PropTypes.bool, 67 | appendToBody: PropTypes.bool, 68 | disableScroll: PropTypes.bool, 69 | daySize: nonNegativeInteger, 70 | isRTL: PropTypes.bool, 71 | firstDayOfWeek: DayOfWeekShape, 72 | initialVisibleMonth: PropTypes.func, 73 | numberOfMonths: PropTypes.number, 74 | keepOpenOnDateSelect: PropTypes.bool, 75 | reopenPickerOnClearDates: PropTypes.bool, 76 | renderCalendarInfo: PropTypes.func, 77 | calendarInfoPosition: CalendarInfoPositionShape, 78 | hideKeyboardShortcutsPanel: PropTypes.bool, 79 | verticalHeight: nonNegativeInteger, 80 | transitionDuration: nonNegativeInteger, 81 | verticalSpacing: nonNegativeInteger, 82 | horizontalMonthPadding: nonNegativeInteger, 83 | 84 | // navigation related props 85 | dayPickerNavigationInlineStyles: PropTypes.object, 86 | navPosition: NavPositionShape, 87 | navPrev: PropTypes.node, 88 | navNext: PropTypes.node, 89 | renderNavPrevButton: PropTypes.func, 90 | renderNavNextButton: PropTypes.func, 91 | onPrevMonthClick: PropTypes.func, 92 | onNextMonthClick: PropTypes.func, 93 | 94 | // day presentation and interaction related props 95 | renderCalendarDay: PropTypes.func, 96 | renderDayContents: PropTypes.func, 97 | minimumNights: PropTypes.number, 98 | minDate: momentPropTypes.momentObj, 99 | maxDate: momentPropTypes.momentObj, 100 | enableOutsideDays: PropTypes.bool, 101 | isDayBlocked: PropTypes.func, 102 | isOutsideRange: PropTypes.func, 103 | isDayHighlighted: PropTypes.func, 104 | 105 | // internationalization props 106 | displayFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 107 | monthFormat: PropTypes.string, 108 | weekDayFormat: PropTypes.string, 109 | phrases: PropTypes.shape(getPhrasePropTypes(DateRangePickerPhrases)), 110 | dayAriaLabelFormat: PropTypes.string, 111 | }; 112 | -------------------------------------------------------------------------------- /src/shapes/DayOfWeekShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { WEEKDAYS } from '../constants'; 4 | 5 | export default PropTypes.oneOf(WEEKDAYS); 6 | -------------------------------------------------------------------------------- /src/shapes/DisabledShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { START_DATE, END_DATE } from '../constants'; 4 | 5 | export default PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf([START_DATE, END_DATE])]); 6 | -------------------------------------------------------------------------------- /src/shapes/FocusedInputShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { 4 | START_DATE, 5 | END_DATE, 6 | } from '../constants'; 7 | 8 | export default PropTypes.oneOf([START_DATE, END_DATE]); 9 | -------------------------------------------------------------------------------- /src/shapes/IconPositionShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { 4 | ICON_BEFORE_POSITION, 5 | ICON_AFTER_POSITION, 6 | } from '../constants'; 7 | 8 | export default PropTypes.oneOf([ICON_BEFORE_POSITION, ICON_AFTER_POSITION]); 9 | -------------------------------------------------------------------------------- /src/shapes/ModifiersShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { and } from 'airbnb-prop-types'; 3 | 4 | export default and([ 5 | PropTypes.instanceOf(Set), 6 | function modifiers(props, propName, ...rest) { 7 | const { [propName]: propValue } = props; 8 | let firstError; 9 | [...propValue].some((v, i) => { 10 | const fakePropName = `${propName}: index ${i}`; 11 | firstError = PropTypes.string.isRequired( 12 | { [fakePropName]: v }, 13 | fakePropName, 14 | ...rest, 15 | ); 16 | return firstError != null; 17 | }); 18 | return firstError == null ? null : firstError; 19 | }, 20 | ], 'Modifiers (Set of Strings)'); 21 | -------------------------------------------------------------------------------- /src/shapes/NavPositionShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { 4 | NAV_POSITION_BOTTOM, 5 | NAV_POSITION_TOP, 6 | } from '../constants'; 7 | 8 | export default PropTypes.oneOf([NAV_POSITION_BOTTOM, NAV_POSITION_TOP]); 9 | -------------------------------------------------------------------------------- /src/shapes/OpenDirectionShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { 4 | OPEN_DOWN, 5 | OPEN_UP, 6 | } from '../constants'; 7 | 8 | export default PropTypes.oneOf([OPEN_DOWN, OPEN_UP]); 9 | -------------------------------------------------------------------------------- /src/shapes/OrientationShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { 4 | HORIZONTAL_ORIENTATION, 5 | VERTICAL_ORIENTATION, 6 | } from '../constants'; 7 | 8 | export default PropTypes.oneOf([HORIZONTAL_ORIENTATION, VERTICAL_ORIENTATION]); 9 | -------------------------------------------------------------------------------- /src/shapes/ScrollableOrientationShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | import { 4 | HORIZONTAL_ORIENTATION, 5 | VERTICAL_ORIENTATION, 6 | VERTICAL_SCROLLABLE, 7 | } from '../constants'; 8 | 9 | export default PropTypes.oneOf([ 10 | HORIZONTAL_ORIENTATION, 11 | VERTICAL_ORIENTATION, 12 | VERTICAL_SCROLLABLE, 13 | ]); 14 | -------------------------------------------------------------------------------- /src/shapes/SingleDatePickerShape.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import momentPropTypes from 'react-moment-proptypes'; 3 | import { mutuallyExclusiveProps, nonNegativeInteger } from 'airbnb-prop-types'; 4 | 5 | import { SingleDatePickerPhrases } from '../defaultPhrases'; 6 | import getPhrasePropTypes from '../utils/getPhrasePropTypes'; 7 | 8 | import IconPositionShape from './IconPositionShape'; 9 | import OrientationShape from './OrientationShape'; 10 | import anchorDirectionShape from './AnchorDirectionShape'; 11 | import openDirectionShape from './OpenDirectionShape'; 12 | import DayOfWeekShape from './DayOfWeekShape'; 13 | import CalendarInfoPositionShape from './CalendarInfoPositionShape'; 14 | import NavPositionShape from './NavPositionShape'; 15 | 16 | export default { 17 | // required props for a functional interactive SingleDatePicker 18 | date: momentPropTypes.momentObj, 19 | onDateChange: PropTypes.func.isRequired, 20 | 21 | focused: PropTypes.bool, 22 | onFocusChange: PropTypes.func.isRequired, 23 | 24 | // input related props 25 | id: PropTypes.string.isRequired, 26 | placeholder: PropTypes.string, 27 | ariaLabel: PropTypes.string, 28 | titleText: PropTypes.string, 29 | disabled: PropTypes.bool, 30 | required: PropTypes.bool, 31 | readOnly: PropTypes.bool, 32 | screenReaderInputMessage: PropTypes.string, 33 | showClearDate: PropTypes.bool, 34 | customCloseIcon: PropTypes.node, 35 | showDefaultInputIcon: PropTypes.bool, 36 | inputIconPosition: IconPositionShape, 37 | customInputIcon: PropTypes.node, 38 | noBorder: PropTypes.bool, 39 | block: PropTypes.bool, 40 | small: PropTypes.bool, 41 | regular: PropTypes.bool, 42 | verticalSpacing: nonNegativeInteger, 43 | keepFocusOnInput: PropTypes.bool, 44 | autoComplete: PropTypes.string, 45 | 46 | // calendar presentation and interaction related props 47 | renderMonthText: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'), 48 | renderMonthElement: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'), 49 | renderWeekHeaderElement: PropTypes.func, 50 | orientation: OrientationShape, 51 | anchorDirection: anchorDirectionShape, 52 | openDirection: openDirectionShape, 53 | horizontalMargin: PropTypes.number, 54 | withPortal: PropTypes.bool, 55 | withFullScreenPortal: PropTypes.bool, 56 | appendToBody: PropTypes.bool, 57 | disableScroll: PropTypes.bool, 58 | initialVisibleMonth: PropTypes.func, 59 | firstDayOfWeek: DayOfWeekShape, 60 | numberOfMonths: PropTypes.number, 61 | keepOpenOnDateSelect: PropTypes.bool, 62 | reopenPickerOnClearDate: PropTypes.bool, 63 | renderCalendarInfo: PropTypes.func, 64 | calendarInfoPosition: CalendarInfoPositionShape, 65 | hideKeyboardShortcutsPanel: PropTypes.bool, 66 | daySize: nonNegativeInteger, 67 | isRTL: PropTypes.bool, 68 | verticalHeight: nonNegativeInteger, 69 | transitionDuration: nonNegativeInteger, 70 | horizontalMonthPadding: nonNegativeInteger, 71 | 72 | // navigation related props 73 | dayPickerNavigationInlineStyles: PropTypes.object, 74 | navPosition: NavPositionShape, 75 | navPrev: PropTypes.node, 76 | navNext: PropTypes.node, 77 | renderNavPrevButton: PropTypes.func, 78 | renderNavNextButton: PropTypes.func, 79 | 80 | onPrevMonthClick: PropTypes.func, 81 | onNextMonthClick: PropTypes.func, 82 | onClose: PropTypes.func, 83 | 84 | // day presentation and interaction related props 85 | renderCalendarDay: PropTypes.func, 86 | renderDayContents: PropTypes.func, 87 | enableOutsideDays: PropTypes.bool, 88 | isDayBlocked: PropTypes.func, 89 | isOutsideRange: PropTypes.func, 90 | isDayHighlighted: PropTypes.func, 91 | minDate: momentPropTypes.momentObj, 92 | maxDate: momentPropTypes.momentObj, 93 | 94 | // internationalization props 95 | displayFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 96 | monthFormat: PropTypes.string, 97 | weekDayFormat: PropTypes.string, 98 | phrases: PropTypes.shape(getPhrasePropTypes(SingleDatePickerPhrases)), 99 | dayAriaLabelFormat: PropTypes.string, 100 | }; 101 | -------------------------------------------------------------------------------- /src/svg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | <svg focusable="false" viewBox="0 0 1000 1000"><path d="M336 275L126 485h806c13 0 23 10 23 23s-10 23-23 23H126l210 210c11 11 11 21 0 32-5 5-10 7-16 7s-11-2-16-7L55 524c-11-11-11-21 0-32l249-249c21-22 53 10 32 32z"/></svg> 2 | -------------------------------------------------------------------------------- /src/svg/arrow-right.svg: -------------------------------------------------------------------------------- 1 | <svg focusable="false" viewBox="0 0 1000 1000"><path d="M694 242l249 250c12 11 12 21 1 32L694 773c-5 5-10 7-16 7s-11-2-16-7c-11-11-11-21 0-32l210-210H68c-13 0-23-10-23-23s10-23 23-23h806L662 275c-21-22 11-54 32-33z"/></svg> 2 | -------------------------------------------------------------------------------- /src/svg/calendar.svg: -------------------------------------------------------------------------------- 1 | <svg focusable="false" viewBox="0 0 1393.1 1500"><path d="m107 1393h241v-241h-241zm295 0h268v-241h-268zm-295-295h241v-268h-241zm295 0h268v-268h-268zm-295-321h241v-241h-241zm616 616h268v-241h-268zm-321-616h268v-241h-268zm643 616h241v-241h-241zm-322-295h268v-268h-268zm-294-723v-241c0-7-3-14-8-19-6-5-12-8-19-8h-54c-7 0-13 3-19 8-5 5-8 12-8 19v241c0 7 3 14 8 19 6 5 12 8 19 8h54c7 0 13-3 19-8 5-5 8-12 8-19zm616 723h241v-268h-241zm-322-321h268v-241h-268zm322 0h241v-241h-241zm27-402v-241c0-7-3-14-8-19-6-5-12-8-19-8h-54c-7 0-13 3-19 8-5 5-8 12-8 19v241c0 7 3 14 8 19 6 5 12 8 19 8h54c7 0 13-3 19-8 5-5 8-12 8-19zm321-54v1072c0 29-11 54-32 75s-46 32-75 32h-1179c-29 0-54-11-75-32s-32-46-32-75v-1072c0-29 11-54 32-75s46-32 75-32h107v-80c0-37 13-68 40-95s57-39 94-39h54c37 0 68 13 95 39 26 26 39 58 39 95v80h321v-80c0-37 13-69 40-95 26-26 57-39 94-39h54c37 0 68 13 94 39s40 58 40 95v80h107c29 0 54 11 75 32s32 46 32 75z"/></svg> 2 | -------------------------------------------------------------------------------- /src/svg/chevron-down.svg: -------------------------------------------------------------------------------- 1 | <svg focusable="false" viewBox="0 0 1000 1000"><path d="M968 289L514 741c-11 11-21 11-32 0L29 289c-4-5-6-11-6-16 0-13 10-23 23-23 6 0 11 2 15 7l437 436 438-436c4-5 9-7 16-7 6 0 11 2 16 7 9 10 9 21 0 32z"/></svg> 2 | -------------------------------------------------------------------------------- /src/svg/chevron-up.svg: -------------------------------------------------------------------------------- 1 | <svg focusable="false" viewBox="0 0 1000 1000"><path d="M32 713l453-453c11-11 21-11 32 0l453 453c5 5 7 10 7 16 0 13-10 23-22 23-7 0-12-2-16-7L501 309 64 745c-4 5-9 7-15 7-7 0-12-2-17-7-9-11-9-21 0-32z"/></svg> 2 | -------------------------------------------------------------------------------- /src/svg/close.svg: -------------------------------------------------------------------------------- 1 | <svg focusable="false" viewBox="0 0 12 12"><path fill-rule="evenodd" d="M11.53.47a.75.75 0 0 0-1.061 0l-4.47 4.47L1.529.47A.75.75 0 1 0 .468 1.531l4.47 4.47-4.47 4.47a.75.75 0 1 0 1.061 1.061l4.47-4.47 4.47 4.47a.75.75 0 1 0 1.061-1.061l-4.47-4.47 4.47-4.47a.75.75 0 0 0 0-1.061z"/></svg> 2 | -------------------------------------------------------------------------------- /src/theme/DefaultTheme.js: -------------------------------------------------------------------------------- 1 | const core = { 2 | white: '#fff', 3 | gray: '#484848', 4 | grayLight: '#82888a', 5 | grayLighter: '#cacccd', 6 | grayLightest: '#f2f2f2', 7 | 8 | borderMedium: '#c4c4c4', 9 | border: '#dbdbdb', 10 | borderLight: '#e4e7e7', 11 | borderLighter: '#eceeee', 12 | borderBright: '#f4f5f5', 13 | 14 | primary: '#00a699', 15 | primaryShade_1: '#33dacd', 16 | primaryShade_2: '#66e2da', 17 | primaryShade_3: '#80e8e0', 18 | primaryShade_4: '#b2f1ec', 19 | primary_dark: '#008489', 20 | 21 | secondary: '#007a87', 22 | 23 | yellow: '#ffe8bc', 24 | yellow_dark: '#ffce71', 25 | }; 26 | 27 | export default { 28 | reactDates: { 29 | zIndex: 0, 30 | border: { 31 | input: { 32 | border: 0, 33 | borderTop: 0, 34 | borderRight: 0, 35 | borderBottom: '2px solid transparent', 36 | borderLeft: 0, 37 | outlineFocused: 0, 38 | borderFocused: 0, 39 | borderTopFocused: 0, 40 | borderLeftFocused: 0, 41 | borderBottomFocused: `2px solid ${core.primary_dark}`, 42 | borderRightFocused: 0, 43 | borderRadius: 0, 44 | }, 45 | pickerInput: { 46 | borderWidth: 1, 47 | borderStyle: 'solid', 48 | borderRadius: 2, 49 | }, 50 | }, 51 | 52 | color: { 53 | core, 54 | 55 | disabled: core.grayLightest, 56 | 57 | background: core.white, 58 | backgroundDark: '#f2f2f2', 59 | backgroundFocused: core.white, 60 | border: 'rgb(219, 219, 219)', 61 | text: core.gray, 62 | textDisabled: core.border, 63 | textFocused: '#007a87', 64 | placeholderText: '#757575', 65 | 66 | outside: { 67 | backgroundColor: core.white, 68 | backgroundColor_active: core.white, 69 | backgroundColor_hover: core.white, 70 | color: core.gray, 71 | color_active: core.gray, 72 | color_hover: core.gray, 73 | }, 74 | 75 | highlighted: { 76 | backgroundColor: core.yellow, 77 | backgroundColor_active: core.yellow_dark, 78 | backgroundColor_hover: core.yellow_dark, 79 | color: core.gray, 80 | color_active: core.gray, 81 | color_hover: core.gray, 82 | }, 83 | 84 | minimumNights: { 85 | backgroundColor: core.white, 86 | backgroundColor_active: core.white, 87 | backgroundColor_hover: core.white, 88 | borderColor: core.borderLighter, 89 | color: core.grayLighter, 90 | color_active: core.grayLighter, 91 | color_hover: core.grayLighter, 92 | }, 93 | 94 | hoveredSpan: { 95 | backgroundColor: core.primaryShade_4, 96 | backgroundColor_active: core.primaryShade_3, 97 | backgroundColor_hover: core.primaryShade_4, 98 | borderColor: core.primaryShade_3, 99 | borderColor_active: core.primaryShade_3, 100 | borderColor_hover: core.primaryShade_3, 101 | color: core.secondary, 102 | color_active: core.secondary, 103 | color_hover: core.secondary, 104 | }, 105 | 106 | selectedSpan: { 107 | backgroundColor: core.primaryShade_2, 108 | backgroundColor_active: core.primaryShade_1, 109 | backgroundColor_hover: core.primaryShade_1, 110 | borderColor: core.primaryShade_1, 111 | borderColor_active: core.primary, 112 | borderColor_hover: core.primary, 113 | color: core.white, 114 | color_active: core.white, 115 | color_hover: core.white, 116 | }, 117 | 118 | selected: { 119 | backgroundColor: core.primary, 120 | backgroundColor_active: core.primary, 121 | backgroundColor_hover: core.primary, 122 | borderColor: core.primary, 123 | borderColor_active: core.primary, 124 | borderColor_hover: core.primary, 125 | color: core.white, 126 | color_active: core.white, 127 | color_hover: core.white, 128 | }, 129 | 130 | blocked_calendar: { 131 | backgroundColor: core.grayLighter, 132 | backgroundColor_active: core.grayLighter, 133 | backgroundColor_hover: core.grayLighter, 134 | borderColor: core.grayLighter, 135 | borderColor_active: core.grayLighter, 136 | borderColor_hover: core.grayLighter, 137 | color: core.grayLight, 138 | color_active: core.grayLight, 139 | color_hover: core.grayLight, 140 | }, 141 | 142 | blocked_out_of_range: { 143 | backgroundColor: core.white, 144 | backgroundColor_active: core.white, 145 | backgroundColor_hover: core.white, 146 | borderColor: core.borderLight, 147 | borderColor_active: core.borderLight, 148 | borderColor_hover: core.borderLight, 149 | color: core.grayLighter, 150 | color_active: core.grayLighter, 151 | color_hover: core.grayLighter, 152 | }, 153 | }, 154 | 155 | spacing: { 156 | dayPickerHorizontalPadding: 9, 157 | captionPaddingTop: 22, 158 | captionPaddingBottom: 37, 159 | inputPadding: 0, 160 | displayTextPaddingVertical: undefined, 161 | displayTextPaddingTop: 11, 162 | displayTextPaddingBottom: 9, 163 | displayTextPaddingHorizontal: undefined, 164 | displayTextPaddingLeft: 11, 165 | displayTextPaddingRight: 11, 166 | displayTextPaddingVertical_small: undefined, 167 | displayTextPaddingTop_small: 7, 168 | displayTextPaddingBottom_small: 5, 169 | displayTextPaddingHorizontal_small: undefined, 170 | displayTextPaddingLeft_small: 7, 171 | displayTextPaddingRight_small: 7, 172 | }, 173 | 174 | sizing: { 175 | inputWidth: 130, 176 | inputWidth_small: 97, 177 | arrowWidth: 24, 178 | }, 179 | 180 | noScrollBarOnVerticalScrollable: false, 181 | 182 | font: { 183 | size: 14, 184 | captionSize: 18, 185 | input: { 186 | size: 19, 187 | weight: 200, 188 | lineHeight: '24px', 189 | size_small: 15, 190 | lineHeight_small: '18px', 191 | letterSpacing_small: '0.2px', 192 | styleDisabled: 'italic', 193 | }, 194 | }, 195 | }, 196 | }; 197 | -------------------------------------------------------------------------------- /src/utils/calculateDimension.js: -------------------------------------------------------------------------------- 1 | export default function calculateDimension(el, axis, borderBox = false, withMargin = false) { 2 | if (!el) { 3 | return 0; 4 | } 5 | 6 | const axisStart = axis === 'width' ? 'Left' : 'Top'; 7 | const axisEnd = axis === 'width' ? 'Right' : 'Bottom'; 8 | 9 | // Only read styles if we need to 10 | const style = (!borderBox || withMargin) ? window.getComputedStyle(el) : null; 11 | 12 | // Offset includes border and padding 13 | const { offsetWidth, offsetHeight } = el; 14 | let size = axis === 'width' ? offsetWidth : offsetHeight; 15 | 16 | // Get the inner size 17 | if (!borderBox) { 18 | size -= ( 19 | parseFloat(style[`padding${axisStart}`]) 20 | + parseFloat(style[`padding${axisEnd}`]) 21 | + parseFloat(style[`border${axisStart}Width`]) 22 | + parseFloat(style[`border${axisEnd}Width`]) 23 | ); 24 | } 25 | 26 | // Apply margin 27 | if (withMargin) { 28 | size += (parseFloat(style[`margin${axisStart}`]) + parseFloat(style[`margin${axisEnd}`])); 29 | } 30 | 31 | return size; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/disableScroll.js: -------------------------------------------------------------------------------- 1 | const getScrollingRoot = () => document.scrollingElement || document.documentElement; 2 | 3 | /** 4 | * Recursively finds the scroll parent of a node. The scroll parrent of a node 5 | * is the closest node that is scrollable. A node is scrollable if: 6 | * - it is allowed to scroll via CSS ('overflow-y' not visible or hidden); 7 | * - and its children/content are "bigger" than the node's box height. 8 | * 9 | * The root of the document always scrolls by default. 10 | * 11 | * @param {HTMLElement} node Any DOM element. 12 | * @return {HTMLElement} The scroll parent element. 13 | */ 14 | export function getScrollParent(node) { 15 | const parent = node.parentElement; 16 | 17 | if (parent == null) return getScrollingRoot(); 18 | 19 | const { overflowY } = window.getComputedStyle(parent); 20 | const canScroll = overflowY !== 'visible' && overflowY !== 'hidden'; 21 | 22 | if (canScroll && parent.scrollHeight > parent.clientHeight) { 23 | return parent; 24 | } 25 | 26 | return getScrollParent(parent); 27 | } 28 | 29 | /** 30 | * Recursively traverses the tree upwards from the given node, capturing all 31 | * ancestor nodes that scroll along with their current 'overflow-y' CSS 32 | * property. 33 | * 34 | * @param {HTMLElement} node Any DOM element. 35 | * @param {Map<HTMLElement,string>} [acc] Accumulator map. 36 | * @return {Map<HTMLElement,string>} Map of ancestors with their 'overflow-y' value. 37 | */ 38 | export function getScrollAncestorsOverflowY(node, acc = new Map()) { 39 | const scrollingRoot = getScrollingRoot(); 40 | const scrollParent = getScrollParent(node); 41 | acc.set(scrollParent, scrollParent.style.overflowY); 42 | 43 | if (scrollParent === scrollingRoot) return acc; 44 | return getScrollAncestorsOverflowY(scrollParent, acc); 45 | } 46 | 47 | /** 48 | * Disabling the scroll on a node involves finding all the scrollable ancestors 49 | * and set their 'overflow-y' CSS property to 'hidden'. When all ancestors have 50 | * 'overflow-y: hidden' (up to the document element) there is no scroll 51 | * container, thus all the scroll outside of the node is disabled. In order to 52 | * enable scroll again, we store the previous value of the 'overflow-y' for 53 | * every ancestor in a closure and reset it back. 54 | * 55 | * @param {HTMLElement} node Any DOM element. 56 | */ 57 | export default function disableScroll(node) { 58 | const scrollAncestorsOverflowY = getScrollAncestorsOverflowY(node); 59 | const toggle = (on) => scrollAncestorsOverflowY.forEach((overflowY, ancestor) => { 60 | ancestor.style.setProperty('overflow-y', on ? 'hidden' : overflowY); 61 | }); 62 | 63 | toggle(true); 64 | return () => toggle(false); 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/getActiveElement.js: -------------------------------------------------------------------------------- 1 | export default function getActiveElement() { 2 | return typeof document !== 'undefined' && document.activeElement; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/getCalendarDaySettings.js: -------------------------------------------------------------------------------- 1 | import getPhrase from './getPhrase'; 2 | import { BLOCKED_MODIFIER } from '../constants'; 3 | 4 | function isSelected(modifiers) { 5 | return modifiers.has('selected') 6 | || modifiers.has('selected-span') 7 | || modifiers.has('selected-start') 8 | || modifiers.has('selected-end'); 9 | } 10 | 11 | function shouldUseDefaultCursor(modifiers) { 12 | return modifiers.has('blocked-minimum-nights') 13 | || modifiers.has('blocked-calendar') 14 | || modifiers.has('blocked-out-of-range'); 15 | } 16 | 17 | function isHoveredSpan(modifiers) { 18 | if (isSelected(modifiers)) return false; 19 | return modifiers.has('hovered-span') || modifiers.has('after-hovered-start') || modifiers.has('before-hovered-end'); 20 | } 21 | 22 | function getAriaLabel(phrases, modifiers, day, ariaLabelFormat) { 23 | const { 24 | chooseAvailableDate, 25 | dateIsUnavailable, 26 | dateIsSelected, 27 | dateIsSelectedAsStartDate, 28 | dateIsSelectedAsEndDate, 29 | } = phrases; 30 | 31 | const formattedDate = { 32 | date: day.format(ariaLabelFormat), 33 | }; 34 | 35 | if (modifiers.has('selected-start') && dateIsSelectedAsStartDate) { 36 | return getPhrase(dateIsSelectedAsStartDate, formattedDate); 37 | } if (modifiers.has('selected-end') && dateIsSelectedAsEndDate) { 38 | return getPhrase(dateIsSelectedAsEndDate, formattedDate); 39 | } if (isSelected(modifiers) && dateIsSelected) { 40 | return getPhrase(dateIsSelected, formattedDate); 41 | } if (modifiers.has(BLOCKED_MODIFIER)) { 42 | return getPhrase(dateIsUnavailable, formattedDate); 43 | } 44 | 45 | return getPhrase(chooseAvailableDate, formattedDate); 46 | } 47 | 48 | export default function getCalendarDaySettings(day, ariaLabelFormat, daySize, modifiers, phrases) { 49 | return { 50 | ariaLabel: getAriaLabel(phrases, modifiers, day, ariaLabelFormat), 51 | hoveredSpan: isHoveredSpan(modifiers), 52 | isOutsideRange: modifiers.has('blocked-out-of-range'), 53 | selected: isSelected(modifiers), 54 | useDefaultCursor: shouldUseDefaultCursor(modifiers), 55 | 56 | daySizeStyles: { 57 | width: daySize, 58 | height: daySize - 1, 59 | }, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/getCalendarMonthWeeks.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { WEEKDAYS } from '../constants'; 4 | 5 | export default function getCalendarMonthWeeks( 6 | month, 7 | enableOutsideDays, 8 | firstDayOfWeek = moment.localeData().firstDayOfWeek(), 9 | ) { 10 | if (!moment.isMoment(month) || !month.isValid()) { 11 | throw new TypeError('`month` must be a valid moment object'); 12 | } 13 | if (WEEKDAYS.indexOf(firstDayOfWeek) === -1) { 14 | throw new TypeError('`firstDayOfWeek` must be an integer between 0 and 6'); 15 | } 16 | 17 | // set utc offset to get correct dates in future (when timezone changes) 18 | const firstOfMonth = month.clone().startOf('month').hour(12); 19 | const lastOfMonth = month.clone().endOf('month').hour(12); 20 | 21 | // calculate the exact first and last days to fill the entire matrix 22 | // (considering days outside month) 23 | const prevDays = ((firstOfMonth.day() + 7 - firstDayOfWeek) % 7); 24 | const nextDays = ((firstDayOfWeek + 6 - lastOfMonth.day()) % 7); 25 | const firstDay = firstOfMonth.clone().subtract(prevDays, 'day'); 26 | const lastDay = lastOfMonth.clone().add(nextDays, 'day'); 27 | 28 | const totalDays = lastDay.diff(firstDay, 'days') + 1; 29 | 30 | const currentDay = firstDay.clone(); 31 | const weeksInMonth = []; 32 | 33 | for (let i = 0; i < totalDays; i += 1) { 34 | if (i % 7 === 0) { 35 | weeksInMonth.push([]); 36 | } 37 | 38 | let day = null; 39 | if ((i >= prevDays && i < (totalDays - nextDays)) || enableOutsideDays) { 40 | day = currentDay.clone(); 41 | } 42 | 43 | weeksInMonth[weeksInMonth.length - 1].push(day); 44 | 45 | currentDay.add(1, 'day'); 46 | } 47 | 48 | return weeksInMonth; 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/getCalendarMonthWidth.js: -------------------------------------------------------------------------------- 1 | export default function getCalendarMonthWidth(daySize, calendarMonthPadding = 0) { 2 | return (7 * daySize) + (2 * calendarMonthPadding) + 1; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/getDetachedContainerStyles.js: -------------------------------------------------------------------------------- 1 | import { OPEN_UP, ANCHOR_RIGHT } from '../constants'; 2 | 3 | /** 4 | * Calculate and return a CSS transform style to position a detached element 5 | * next to a reference element. The open and anchor direction indicate wether 6 | * it should be positioned above/below and/or to the left/right of the 7 | * reference element. 8 | * 9 | * Assuming r(0,0), r(1,1), d(0,0), d(1,1) for the bottom-left and top-right 10 | * corners of the reference and detached elements, respectively: 11 | * - openDirection = DOWN, anchorDirection = LEFT => d(0,1) == r(0,1) 12 | * - openDirection = UP, anchorDirection = LEFT => d(0,0) == r(0,0) 13 | * - openDirection = DOWN, anchorDirection = RIGHT => d(1,1) == r(1,1) 14 | * - openDirection = UP, anchorDirection = RIGHT => d(1,0) == r(1,0) 15 | * 16 | * By using a CSS transform, we allow to further position it using 17 | * top/bottom CSS properties for the anchor gutter. 18 | * 19 | * @param {string} openDirection The vertical positioning of the popup 20 | * @param {string} anchorDirection The horizontal position of the popup 21 | * @param {HTMLElement} referenceEl The reference element 22 | */ 23 | export default function getDetachedContainerStyles(openDirection, anchorDirection, referenceEl) { 24 | const referenceRect = referenceEl.getBoundingClientRect(); 25 | let offsetX = referenceRect.left; 26 | let offsetY = referenceRect.top; 27 | 28 | if (openDirection === OPEN_UP) { 29 | offsetY = -(window.innerHeight - referenceRect.bottom); 30 | } 31 | 32 | if (anchorDirection === ANCHOR_RIGHT) { 33 | offsetX = -(window.innerWidth - referenceRect.right); 34 | } 35 | 36 | return { 37 | transform: `translate3d(${Math.round(offsetX)}px, ${Math.round(offsetY)}px, 0)`, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/getInputHeight.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | function getPadding(vertical, top, bottom) { 4 | const isTopDefined = typeof top === 'number'; 5 | const isBottomDefined = typeof bottom === 'number'; 6 | const isVerticalDefined = typeof vertical === 'number'; 7 | 8 | if (isTopDefined && isBottomDefined) { 9 | return top + bottom; 10 | } 11 | 12 | if (isTopDefined && isVerticalDefined) { 13 | return top + vertical; 14 | } 15 | 16 | if (isTopDefined) { 17 | return top; 18 | } 19 | 20 | if (isBottomDefined && isVerticalDefined) { 21 | return bottom + vertical; 22 | } 23 | 24 | if (isBottomDefined) { 25 | return bottom; 26 | } 27 | 28 | if (isVerticalDefined) { 29 | return 2 * vertical; 30 | } 31 | 32 | return 0; 33 | } 34 | 35 | export default function getInputHeight({ 36 | font: { 37 | input: { 38 | lineHeight, 39 | lineHeight_small, 40 | }, 41 | }, 42 | spacing: { 43 | inputPadding, 44 | displayTextPaddingVertical, 45 | displayTextPaddingTop, 46 | displayTextPaddingBottom, 47 | displayTextPaddingVertical_small, 48 | displayTextPaddingTop_small, 49 | displayTextPaddingBottom_small, 50 | }, 51 | }, small) { 52 | const calcLineHeight = small ? lineHeight_small : lineHeight; 53 | 54 | const padding = small 55 | ? getPadding( 56 | displayTextPaddingVertical_small, 57 | displayTextPaddingTop_small, 58 | displayTextPaddingBottom_small, 59 | ) 60 | : getPadding( 61 | displayTextPaddingVertical, 62 | displayTextPaddingTop, 63 | displayTextPaddingBottom, 64 | ); 65 | 66 | return parseInt(calcLineHeight, 10) + (2 * inputPadding) + padding; 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/getNumberOfCalendarMonthWeeks.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | function getBlankDaysBeforeFirstDay(firstDayOfMonth, firstDayOfWeek) { 4 | const weekDayDiff = firstDayOfMonth.day() - firstDayOfWeek; 5 | return (weekDayDiff + 7) % 7; 6 | } 7 | 8 | export default function getNumberOfCalendarMonthWeeks( 9 | month, 10 | firstDayOfWeek = moment.localeData().firstDayOfWeek(), 11 | ) { 12 | const firstDayOfMonth = month.clone().startOf('month').hour(12); 13 | const numBlankDays = getBlankDaysBeforeFirstDay(firstDayOfMonth, firstDayOfWeek); 14 | return Math.ceil((numBlankDays + month.daysInMonth()) / 7); 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/getPhrase.jsx: -------------------------------------------------------------------------------- 1 | export default function getPhrase(phrase, args) { 2 | if (typeof phrase === 'string') return phrase; 3 | 4 | if (typeof phrase === 'function') { 5 | return phrase(args); 6 | } 7 | 8 | return ''; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/getPhrasePropTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default function getPhrasePropTypes(defaultPhrases) { 4 | return Object.keys(defaultPhrases) 5 | .reduce((phrases, key) => ({ 6 | ...phrases, 7 | [key]: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]), 8 | }), {}); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/getPooledMoment.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | const momentPool = new Map(); 4 | export default function getPooledMoment(dayString) { 5 | if (!momentPool.has(dayString)) { 6 | momentPool.set(dayString, moment(dayString)); 7 | } 8 | 9 | return momentPool.get(dayString); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/getPreviousMonthMemoLast.js: -------------------------------------------------------------------------------- 1 | let getPreviousMonthMemoKey; 2 | let getPreviousMonthMemoValue; 3 | 4 | export default function getPreviousMonthMemoLast(month) { 5 | if (month !== getPreviousMonthMemoKey) { 6 | getPreviousMonthMemoKey = month; 7 | getPreviousMonthMemoValue = month.clone().subtract(1, 'month'); 8 | } 9 | 10 | return getPreviousMonthMemoValue; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/getResponsiveContainerStyles.js: -------------------------------------------------------------------------------- 1 | import { ANCHOR_LEFT } from '../constants'; 2 | 3 | export default function getResponsiveContainerStyles( 4 | anchorDirection, 5 | currentOffset, 6 | containerEdge, 7 | margin, 8 | ) { 9 | const windowWidth = typeof window !== 'undefined' ? window.innerWidth : 0; 10 | const calculatedOffset = anchorDirection === ANCHOR_LEFT 11 | ? windowWidth - containerEdge 12 | : containerEdge; 13 | const calculatedMargin = margin || 0; 14 | 15 | return { 16 | [anchorDirection]: Math.min(currentOffset + calculatedOffset - calculatedMargin, 0), 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/getSelectedDateOffset.js: -------------------------------------------------------------------------------- 1 | const defaultModifier = (day) => day; 2 | 3 | export default function getSelectedDateOffset(fn, day, modifier = defaultModifier) { 4 | if (!fn) return day; 5 | return modifier(fn(day.clone())); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/getTransformStyles.js: -------------------------------------------------------------------------------- 1 | export default function getTransformStyles(transformValue) { 2 | return { 3 | transform: transformValue, 4 | msTransform: transformValue, 5 | MozTransform: transformValue, 6 | WebkitTransform: transformValue, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/getVisibleDays.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import toISOMonthString from './toISOMonthString'; 3 | 4 | export default function getVisibleDays( 5 | month, 6 | numberOfMonths, 7 | enableOutsideDays, 8 | withoutTransitionMonths, 9 | ) { 10 | if (!moment.isMoment(month)) return {}; 11 | 12 | const visibleDaysByMonth = {}; 13 | let currentMonth = withoutTransitionMonths ? month.clone() : month.clone().subtract(1, 'month'); 14 | for (let i = 0; i < (withoutTransitionMonths ? numberOfMonths : numberOfMonths + 2); i += 1) { 15 | const visibleDays = []; 16 | 17 | // set utc offset to get correct dates in future (when timezone changes) 18 | const baseDate = currentMonth.clone(); 19 | const firstOfMonth = baseDate.clone().startOf('month').hour(12); 20 | const lastOfMonth = baseDate.clone().endOf('month').hour(12); 21 | 22 | const currentDay = firstOfMonth.clone(); 23 | 24 | // days belonging to the previous month 25 | if (enableOutsideDays) { 26 | for (let j = 0; j < currentDay.weekday(); j += 1) { 27 | const prevDay = currentDay.clone().subtract(j + 1, 'day'); 28 | visibleDays.unshift(prevDay); 29 | } 30 | } 31 | 32 | while (currentDay < lastOfMonth) { 33 | visibleDays.push(currentDay.clone()); 34 | currentDay.add(1, 'day'); 35 | } 36 | 37 | if (enableOutsideDays) { 38 | // weekday() returns the index of the day of the week according to the locale 39 | // this means if the week starts on Monday, weekday() will return 0 for a Monday date, not 1 40 | if (currentDay.weekday() !== 0) { 41 | // days belonging to the next month 42 | for (let k = currentDay.weekday(), count = 0; k < 7; k += 1, count += 1) { 43 | const nextDay = currentDay.clone().add(count, 'day'); 44 | visibleDays.push(nextDay); 45 | } 46 | } 47 | } 48 | 49 | visibleDaysByMonth[toISOMonthString(currentMonth)] = visibleDays; 50 | currentMonth = currentMonth.clone().add(1, 'month'); 51 | } 52 | 53 | return visibleDaysByMonth; 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/isAfterDay.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import isBeforeDay from './isBeforeDay'; 4 | import isSameDay from './isSameDay'; 5 | 6 | export default function isAfterDay(a, b) { 7 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 8 | return !isBeforeDay(a, b) && !isSameDay(a, b); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/isBeforeDay.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export default function isBeforeDay(a, b) { 4 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 5 | 6 | const aYear = a.year(); 7 | const aMonth = a.month(); 8 | 9 | const bYear = b.year(); 10 | const bMonth = b.month(); 11 | 12 | const isSameYear = aYear === bYear; 13 | const isSameMonth = aMonth === bMonth; 14 | 15 | if (isSameYear && isSameMonth) return a.date() < b.date(); 16 | if (isSameYear) return aMonth < bMonth; 17 | return aYear < bYear; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/isDayVisible.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import isBeforeDay from './isBeforeDay'; 4 | import isAfterDay from './isAfterDay'; 5 | import toISOMonthString from './toISOMonthString'; 6 | 7 | const startCacheOutsideDays = new Map(); 8 | const endCacheOutsideDays = new Map(); 9 | 10 | const startCacheInsideDays = new Map(); 11 | const endCacheInsideDays = new Map(); 12 | 13 | export default function isDayVisible(day, month, numberOfMonths, enableOutsideDays) { 14 | if (!moment.isMoment(day)) return false; 15 | 16 | // Cloning is a little expensive, so we want to do it as little as possible. 17 | 18 | const startKey = toISOMonthString(month); 19 | // eslint-disable-next-line prefer-template 20 | const endKey = startKey + '+' + numberOfMonths; 21 | 22 | if (enableOutsideDays) { 23 | if (!startCacheOutsideDays.has(startKey)) { 24 | startCacheOutsideDays.set(startKey, month.clone().startOf('month').startOf('week').hour(12)); 25 | } 26 | 27 | if (isBeforeDay(day, startCacheOutsideDays.get(startKey))) return false; 28 | 29 | if (!endCacheOutsideDays.has(endKey)) { 30 | endCacheOutsideDays.set( 31 | endKey, 32 | month.clone().endOf('week').add(numberOfMonths - 1, 'months').endOf('month') 33 | .endOf('week') 34 | .hour(12), 35 | ); 36 | } 37 | 38 | return !isAfterDay(day, endCacheOutsideDays.get(endKey)); 39 | } 40 | 41 | // !enableOutsideDays 42 | 43 | if (!startCacheInsideDays.has(startKey)) { 44 | startCacheInsideDays.set(startKey, month.clone().startOf('month').hour(12)); 45 | } 46 | 47 | if (isBeforeDay(day, startCacheInsideDays.get(startKey))) return false; 48 | 49 | if (!endCacheInsideDays.has(endKey)) { 50 | endCacheInsideDays.set( 51 | endKey, 52 | month.clone().add(numberOfMonths - 1, 'months').endOf('month').hour(12), 53 | ); 54 | } 55 | 56 | return !isAfterDay(day, endCacheInsideDays.get(endKey)); 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/isInclusivelyAfterDay.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import isBeforeDay from './isBeforeDay'; 4 | 5 | export default function isInclusivelyAfterDay(a, b) { 6 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 7 | return !isBeforeDay(a, b); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/isInclusivelyBeforeDay.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import isAfterDay from './isAfterDay'; 4 | 5 | export default function isInclusivelyBeforeDay(a, b) { 6 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 7 | return !isAfterDay(a, b); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/isNextDay.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import isSameDay from './isSameDay'; 4 | 5 | export default function isNextDay(a, b) { 6 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 7 | const nextDay = moment(a).add(1, 'day'); 8 | return isSameDay(nextDay, b); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/isNextMonth.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import isSameMonth from './isSameMonth'; 4 | 5 | export default function isNextMonth(a, b) { 6 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 7 | return isSameMonth(a.clone().add(1, 'month'), b); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/isPrevMonth.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import isSameMonth from './isSameMonth'; 4 | 5 | export default function isPrevMonth(a, b) { 6 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 7 | return isSameMonth(a.clone().subtract(1, 'month'), b); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/isPreviousDay.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import isSameDay from './isSameDay'; 4 | 5 | export default function isPreviousDay(a, b) { 6 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 7 | const dayBefore = moment(a).subtract(1, 'day'); 8 | return isSameDay(dayBefore, b); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/isSameDay.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export default function isSameDay(a, b) { 4 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 5 | // Compare least significant, most likely to change units first 6 | // Moment's isSame clones moment inputs and is a tad slow 7 | return a.date() === b.date() 8 | && a.month() === b.month() 9 | && a.year() === b.year(); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/isSameMonth.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export default function isSameMonth(a, b) { 4 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 5 | // Compare least significant, most likely to change units first 6 | // Moment's isSame clones moment inputs and is a tad slow 7 | return a.month() === b.month() 8 | && a.year() === b.year(); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/isTransitionEndSupported.js: -------------------------------------------------------------------------------- 1 | export default function isTransitionEndSupported() { 2 | return !!(typeof window !== 'undefined' && 'TransitionEvent' in window); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/modifiers.js: -------------------------------------------------------------------------------- 1 | import isDayVisible from './isDayVisible'; 2 | import toISODateString from './toISODateString'; 3 | import toISOMonthString from './toISOMonthString'; 4 | import getPreviousMonthMemoLast from './getPreviousMonthMemoLast'; 5 | 6 | import { VERTICAL_SCROLLABLE } from '../constants'; 7 | 8 | export function addModifier(updatedDays, day, modifier, props, state) { 9 | const { numberOfMonths: numberOfVisibleMonths, enableOutsideDays, orientation } = props; 10 | const { currentMonth: firstVisibleMonth, visibleDays } = state; 11 | 12 | let currentMonth = firstVisibleMonth; 13 | let numberOfMonths = numberOfVisibleMonths; 14 | if (orientation === VERTICAL_SCROLLABLE) { 15 | numberOfMonths = Object.keys(visibleDays).length; 16 | } else { 17 | currentMonth = getPreviousMonthMemoLast(currentMonth); 18 | numberOfMonths += 2; 19 | } 20 | if (!day || !isDayVisible(day, currentMonth, numberOfMonths, enableOutsideDays)) { 21 | return updatedDays; 22 | } 23 | 24 | const iso = toISODateString(day); 25 | 26 | let updatedDaysAfterAddition = { ...updatedDays }; 27 | if (enableOutsideDays) { 28 | const monthsToUpdate = Object.keys(visibleDays).filter((monthKey) => ( 29 | Object.keys(visibleDays[monthKey]).indexOf(iso) > -1 30 | )); 31 | 32 | updatedDaysAfterAddition = monthsToUpdate.reduce((acc, monthIso) => { 33 | const month = updatedDays[monthIso] || visibleDays[monthIso]; 34 | 35 | if (!month[iso] || !month[iso].has(modifier)) { 36 | const modifiers = new Set(month[iso]); 37 | modifiers.add(modifier); 38 | acc[monthIso] = { 39 | ...month, 40 | [iso]: modifiers, 41 | }; 42 | } 43 | 44 | return acc; 45 | }, updatedDaysAfterAddition); 46 | } else { 47 | const monthIso = toISOMonthString(day); 48 | const month = updatedDays[monthIso] || visibleDays[monthIso] || {}; 49 | 50 | if (!month[iso] || !month[iso].has(modifier)) { 51 | const modifiers = new Set(month[iso]); 52 | modifiers.add(modifier); 53 | updatedDaysAfterAddition[monthIso] = { 54 | ...month, 55 | [iso]: modifiers, 56 | }; 57 | } 58 | } 59 | 60 | return updatedDaysAfterAddition; 61 | } 62 | 63 | export function deleteModifier(updatedDays, day, modifier, props, state) { 64 | const { numberOfMonths: numberOfVisibleMonths, enableOutsideDays, orientation } = props; 65 | const { currentMonth: firstVisibleMonth, visibleDays } = state; 66 | 67 | let currentMonth = firstVisibleMonth; 68 | let numberOfMonths = numberOfVisibleMonths; 69 | if (orientation === VERTICAL_SCROLLABLE) { 70 | numberOfMonths = Object.keys(visibleDays).length; 71 | } else { 72 | currentMonth = getPreviousMonthMemoLast(currentMonth); 73 | numberOfMonths += 2; 74 | } 75 | if (!day || !isDayVisible(day, currentMonth, numberOfMonths, enableOutsideDays)) { 76 | return updatedDays; 77 | } 78 | 79 | const iso = toISODateString(day); 80 | 81 | let updatedDaysAfterDeletion = { ...updatedDays }; 82 | if (enableOutsideDays) { 83 | const monthsToUpdate = Object.keys(visibleDays).filter((monthKey) => ( 84 | Object.keys(visibleDays[monthKey]).indexOf(iso) > -1 85 | )); 86 | 87 | updatedDaysAfterDeletion = monthsToUpdate.reduce((acc, monthIso) => { 88 | const month = updatedDays[monthIso] || visibleDays[monthIso]; 89 | 90 | if (month[iso] && month[iso].has(modifier)) { 91 | const modifiers = new Set(month[iso]); 92 | modifiers.delete(modifier); 93 | acc[monthIso] = { 94 | ...month, 95 | [iso]: modifiers, 96 | }; 97 | } 98 | 99 | return acc; 100 | }, updatedDaysAfterDeletion); 101 | } else { 102 | const monthIso = toISOMonthString(day); 103 | const month = updatedDays[monthIso] || visibleDays[monthIso] || {}; 104 | 105 | if (month[iso] && month[iso].has(modifier)) { 106 | const modifiers = new Set(month[iso]); 107 | modifiers.delete(modifier); 108 | updatedDaysAfterDeletion[monthIso] = { 109 | ...month, 110 | [iso]: modifiers, 111 | }; 112 | } 113 | } 114 | 115 | return updatedDaysAfterDeletion; 116 | } 117 | -------------------------------------------------------------------------------- /src/utils/noflip.js: -------------------------------------------------------------------------------- 1 | const NOFLIP = '/* @noflip */'; 2 | 3 | // Appends a noflip comment to a style rule in order to prevent it from being automatically 4 | // flipped in RTL contexts. This should be used only in situations where the style must remain 5 | // unflipped regardless of direction context. See: https://github.com/kentcdodds/rtl-css-js#usage 6 | export default function noflip(value) { 7 | if (typeof value === 'number') return `${value}px ${NOFLIP}`; 8 | if (typeof value === 'string') return `${value} ${NOFLIP}`; 9 | 10 | throw new TypeError('noflip expects a string or a number'); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/registerCSSInterfaceWithDefaultTheme.js: -------------------------------------------------------------------------------- 1 | import CSSInterface from 'react-with-styles-interface-css'; 2 | 3 | import registerInterfaceWithDefaultTheme from './registerInterfaceWithDefaultTheme'; 4 | 5 | export default function registerCSSInterfaceWithDefaultTheme() { 6 | registerInterfaceWithDefaultTheme(CSSInterface); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/registerInterfaceWithDefaultTheme.js: -------------------------------------------------------------------------------- 1 | import ThemedStyleSheet from 'react-with-styles/lib/ThemedStyleSheet'; 2 | import DefaultTheme from '../theme/DefaultTheme'; 3 | 4 | export default function registerInterfaceWithDefaultTheme(reactWithStylesInterface) { 5 | ThemedStyleSheet.registerInterface(reactWithStylesInterface); 6 | ThemedStyleSheet.registerTheme(DefaultTheme); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/toISODateString.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import toMomentObject from './toMomentObject'; 4 | 5 | export default function toISODateString(date, currentFormat) { 6 | const dateObj = moment.isMoment(date) ? date : toMomentObject(date, currentFormat); 7 | if (!dateObj) return null; 8 | 9 | // Template strings compiled in strict mode uses concat, which is slow. Since 10 | // this code is in a hot path and we want it to be as fast as possible, we 11 | // want to use old-fashioned +. 12 | // eslint-disable-next-line prefer-template 13 | return dateObj.year() + '-' + String(dateObj.month() + 1).padStart(2, '0') + '-' + String(dateObj.date()).padStart(2, '0'); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/toISOMonthString.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import toMomentObject from './toMomentObject'; 4 | 5 | export default function toISOMonthString(date, currentFormat) { 6 | const dateObj = moment.isMoment(date) ? date : toMomentObject(date, currentFormat); 7 | if (!dateObj) return null; 8 | 9 | // Template strings compiled in strict mode uses concat, which is slow. Since 10 | // this code is in a hot path and we want it to be as fast as possible, we 11 | // want to use old-fashioned +. 12 | // eslint-disable-next-line prefer-template 13 | return dateObj.year() + '-' + String(dateObj.month() + 1).padStart(2, '0'); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/toLocalizedDateString.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import toMomentObject from './toMomentObject'; 4 | 5 | import { DISPLAY_FORMAT } from '../constants'; 6 | 7 | export default function toLocalizedDateString(date, currentFormat) { 8 | const dateObj = moment.isMoment(date) ? date : toMomentObject(date, currentFormat); 9 | if (!dateObj) return null; 10 | 11 | return dateObj.format(DISPLAY_FORMAT); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/toMomentObject.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { DISPLAY_FORMAT, ISO_FORMAT } from '../constants'; 4 | 5 | export default function toMomentObject(dateString, customFormat) { 6 | const dateFormats = customFormat 7 | ? [customFormat, DISPLAY_FORMAT, ISO_FORMAT] 8 | : [DISPLAY_FORMAT, ISO_FORMAT]; 9 | 10 | const date = moment(dateString, dateFormats, true); 11 | return date.isValid() ? date.hour(12) : null; 12 | } 13 | -------------------------------------------------------------------------------- /stories/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": [2, { 4 | "devDependencies": true 5 | }], 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /stories/DateRangePicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import momentJalaali from 'moment-jalaali'; 4 | import { storiesOf } from '@storybook/react'; 5 | import { withInfo } from '@storybook/addon-info'; 6 | import DirectionProvider, { DIRECTIONS } from 'react-with-direction/dist/DirectionProvider'; 7 | 8 | import { 9 | VERTICAL_ORIENTATION, 10 | ANCHOR_RIGHT, 11 | } from '../src/constants'; 12 | 13 | import DateRangePickerWrapper from '../examples/DateRangePickerWrapper'; 14 | 15 | const TestInput = props => ( 16 | <div style={{ marginTop: 16 }}> 17 | <input 18 | {...props} 19 | type="text" 20 | style={{ 21 | height: 48, 22 | width: 284, 23 | fontSize: 18, 24 | fontWeight: 200, 25 | padding: '12px 16px', 26 | }} 27 | /> 28 | </div> 29 | ); 30 | 31 | class TestWrapper extends React.Component { 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | showDatePicker: false, 36 | }; 37 | } 38 | 39 | render() { 40 | const { showDatePicker } = this.state; 41 | const display = showDatePicker ? 'block' : 'none'; 42 | return ( 43 | <div> 44 | <button 45 | type="button" 46 | onClick={() => this.setState({ showDatePicker: !showDatePicker })} 47 | > 48 | Show me! 49 | </button> 50 | 51 | <div style={{ display }}> 52 | <DateRangePickerWrapper /> 53 | </div> 54 | </div> 55 | ); 56 | } 57 | } 58 | 59 | storiesOf('DateRangePicker (DRP)', module) 60 | .add('default', withInfo()(() => ( 61 | <DateRangePickerWrapper /> 62 | ))) 63 | .add('hidden with display: none', withInfo()(() => ( 64 | <TestWrapper /> 65 | ))) 66 | .add('as part of a form', withInfo()(() => ( 67 | <div> 68 | <DateRangePickerWrapper /> 69 | <TestInput placeholder="Input 1" /> 70 | <TestInput placeholder="Input 2" /> 71 | <TestInput placeholder="Input 3" /> 72 | </div> 73 | ))) 74 | .add('non-english locale', withInfo()(() => { 75 | moment.locale('zh-cn'); 76 | return ( 77 | <DateRangePickerWrapper 78 | showClearDates 79 | startDatePlaceholderText="入住日期" 80 | endDatePlaceholderText="退房日期" 81 | monthFormat="YYYY[年]MMMM" 82 | phrases={{ 83 | closeDatePicker: '关闭', 84 | clearDates: '清除日期', 85 | }} 86 | /> 87 | ); 88 | })) 89 | .add('non-english locale (Persian)', withInfo()(() => { 90 | moment.locale('fa'); 91 | momentJalaali.loadPersian({ dialect: 'persian-modern', usePersianDigits: true }); 92 | return ( 93 | <DateRangePickerWrapper 94 | isRTL 95 | stateDateWrapper={momentJalaali} 96 | startDatePlaceholderText="تاریخ شروع" 97 | endDatePlaceholderText="تاریخ پایان" 98 | renderMonthText={month => momentJalaali(month).format('jMMMM jYYYY')} 99 | renderDayContents={day => momentJalaali(day).format('jD')} 100 | /> 101 | ); 102 | })) 103 | .add('with DirectionProvider', withInfo()(() => ( 104 | <DirectionProvider direction={DIRECTIONS.RTL}> 105 | <DateRangePickerWrapper 106 | startDatePlaceholderText="تاریخ شروع" 107 | endDatePlaceholderText="تاریخ پایان" 108 | anchorDirection={ANCHOR_RIGHT} 109 | showDefaultInputIcon 110 | showClearDates 111 | isRTL 112 | /> 113 | </DirectionProvider> 114 | ))) 115 | .add('vertical with custom height', withInfo()(() => ( 116 | <DateRangePickerWrapper 117 | orientation={VERTICAL_ORIENTATION} 118 | verticalHeight={568} 119 | /> 120 | ))) 121 | .add('with navigation blocked (minDate and maxDate)', withInfo()(() => ( 122 | <DateRangePickerWrapper 123 | minDate={moment().subtract(2, 'months').startOf('month')} 124 | maxDate={moment().add(2, 'months').endOf('month')} 125 | numberOfMonths={2} 126 | /> 127 | ))) 128 | .add('with custom autoComplete', withInfo()(() => ( 129 | <DateRangePickerWrapper autoComplete="datePicker" /> 130 | ))); 131 | -------------------------------------------------------------------------------- /stories/DateRangePicker_day.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import isSameDay from '../src/utils/isSameDay'; 7 | import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; 8 | 9 | import CustomizableCalendarDay from '../src/components/CustomizableCalendarDay'; 10 | 11 | import DateRangePickerWrapper from '../examples/DateRangePickerWrapper'; 12 | 13 | const datesList = [ 14 | moment(), 15 | moment().add(1, 'days'), 16 | moment().add(3, 'days'), 17 | moment().add(9, 'days'), 18 | moment().add(10, 'days'), 19 | moment().add(11, 'days'), 20 | moment().add(12, 'days'), 21 | moment().add(13, 'days'), 22 | ]; 23 | 24 | const selectedStyles = { 25 | background: '#590098', 26 | border: '1px solid #590098', 27 | color: '#fff', 28 | 29 | hover: { 30 | background: '#7A32AC', 31 | border: '1px solid #7A32AC', 32 | color: '#fff', 33 | }, 34 | }; 35 | 36 | const hoveredStyles = { 37 | background: '#cd99d0', 38 | border: '1px solid #cd99d0', 39 | color: '#fff', 40 | }; 41 | 42 | const blockedStyles = { 43 | background: '#fff', 44 | border: '1px double #e4e7e7', 45 | color: '#dce0e0', 46 | 47 | hover: { 48 | background: '#fff', 49 | border: '1px double #e4e7e7', 50 | color: '#dce0e0', 51 | }, 52 | }; 53 | 54 | const customDayStyles = { 55 | selectedStartStyles: selectedStyles, 56 | selectedEndStyles: selectedStyles, 57 | hoveredSpanStyles: hoveredStyles, 58 | afterHoveredStartStyles: hoveredStyles, 59 | blockedMinNightsStyles: blockedStyles, 60 | blockedCalendarStyles: blockedStyles, 61 | blockedOutOfRangeStyles: blockedStyles, 62 | 63 | selectedSpanStyles: { 64 | background: '#9b32a2', 65 | border: '1px solid #9b32a2', 66 | color: '#fff', 67 | 68 | hover: { 69 | background: '#83008b', 70 | border: '1px solid #83008b', 71 | color: '#fff', 72 | }, 73 | }, 74 | }; 75 | 76 | storiesOf('DRP - Day Props', module) 77 | .add('default', withInfo()(() => ( 78 | <DateRangePickerWrapper autoFocus /> 79 | ))) 80 | .add('with minimum nights set', withInfo()(() => ( 81 | <DateRangePickerWrapper 82 | minimumNights={3} 83 | initialStartDate={moment().add(3, 'days')} 84 | autoFocusEndDate 85 | /> 86 | ))) 87 | .add('allows single day range', withInfo()(() => ( 88 | <DateRangePickerWrapper 89 | minimumNights={0} 90 | initialStartDate={moment().add(3, 'days')} 91 | autoFocusEndDate 92 | /> 93 | ))) 94 | .add('allows all days, including past days', withInfo()(() => ( 95 | <DateRangePickerWrapper 96 | isOutsideRange={() => false} 97 | autoFocus 98 | /> 99 | ))) 100 | .add('allows next two weeks only', withInfo()(() => ( 101 | <DateRangePickerWrapper 102 | isOutsideRange={day => 103 | !isInclusivelyAfterDay(day, moment()) || 104 | isInclusivelyAfterDay(day, moment().add(2, 'weeks')) 105 | } 106 | autoFocus 107 | /> 108 | ))) 109 | .add('with some blocked dates', withInfo()(() => ( 110 | <DateRangePickerWrapper 111 | isDayBlocked={day1 => datesList.some(day2 => isSameDay(day1, day2))} 112 | autoFocus 113 | /> 114 | ))) 115 | .add('with some highlighted dates', withInfo()(() => ( 116 | <DateRangePickerWrapper 117 | isDayHighlighted={day1 => datesList.some(day2 => isSameDay(day1, day2))} 118 | autoFocus 119 | /> 120 | ))) 121 | .add('blocks fridays', withInfo()(() => ( 122 | <DateRangePickerWrapper 123 | isDayBlocked={day => moment.weekdays(day.weekday()) === 'Friday'} 124 | autoFocus 125 | /> 126 | ))) 127 | .add('with custom daily details', withInfo()(() => ( 128 | <DateRangePickerWrapper 129 | renderDayContents={day => <div className="foo-bar">{day.format('ddd')}</div>} 130 | autoFocus 131 | /> 132 | ))) 133 | .add('one-off custom styling', withInfo()(() => ( 134 | <DateRangePickerWrapper 135 | minimumNights={3} 136 | renderCalendarDay={props => <CustomizableCalendarDay {...props} {...customDayStyles} />} 137 | autoFocus 138 | /> 139 | ))); 140 | -------------------------------------------------------------------------------- /stories/DayPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { withInfo } from '@storybook/addon-info'; 4 | import DirectionProvider, { DIRECTIONS } from 'react-with-direction/dist/DirectionProvider'; 5 | import DayPicker from '../src/components/DayPicker'; 6 | 7 | import { 8 | VERTICAL_ORIENTATION, 9 | VERTICAL_SCROLLABLE, 10 | } from '../src/constants'; 11 | 12 | const TestPrevIcon = () => ( 13 | <div 14 | style={{ 15 | border: '1px solid #dce0e0', 16 | backgroundColor: '#fff', 17 | color: '#484848', 18 | left: '22px', 19 | padding: '3px', 20 | position: 'absolute', 21 | top: '20px', 22 | width: '40px', 23 | }} 24 | tabIndex="0" 25 | > 26 | Prev 27 | </div> 28 | ); 29 | 30 | const TestNextIcon = () => ( 31 | <div 32 | style={{ 33 | border: '1px solid #dce0e0', 34 | backgroundColor: '#fff', 35 | color: '#484848', 36 | padding: '3px', 37 | position: 'absolute', 38 | right: '22px', 39 | top: '20px', 40 | width: '40px', 41 | }} 42 | tabIndex="0" 43 | > 44 | Next 45 | </div> 46 | ); 47 | 48 | const TestCustomInfoPanel = () => ( 49 | <div 50 | style={{ 51 | padding: '10px 21px', 52 | borderTop: '1px solid #dce0e0', 53 | color: '#484848', 54 | }} 55 | > 56 | ❕ Some useful info here 57 | </div> 58 | ); 59 | 60 | function renderNavPrevButton(buttonProps) { 61 | const { 62 | ariaLabel, 63 | disabled, 64 | onClick, 65 | onKeyUp, 66 | onMouseUp, 67 | } = buttonProps; 68 | 69 | return ( 70 | <button 71 | aria-label={ariaLabel} 72 | disabled={disabled} 73 | onClick={onClick} 74 | onKeyUp={onKeyUp} 75 | onMouseUp={onMouseUp} 76 | style={{ position: 'absolute', top: 23, left: 22 }} 77 | type="button" 78 | > 79 | ‹ Prev 80 | </button> 81 | ); 82 | } 83 | 84 | function renderNavNextButton(buttonProps) { 85 | const { 86 | ariaLabel, 87 | disabled, 88 | onClick, 89 | onKeyUp, 90 | onMouseUp, 91 | } = buttonProps; 92 | 93 | return ( 94 | <button 95 | aria-label={ariaLabel} 96 | disabled={disabled} 97 | onClick={onClick} 98 | onKeyUp={onKeyUp} 99 | onMouseUp={onMouseUp} 100 | style={{ position: 'absolute', top: 23, right: 22 }} 101 | type="button" 102 | > 103 | Next › 104 | </button> 105 | ); 106 | } 107 | 108 | storiesOf('DayPicker', module) 109 | .add('default', withInfo()(() => ( 110 | <DayPicker /> 111 | ))) 112 | .add('with custom day size', withInfo()(() => ( 113 | <DayPicker daySize={50} /> 114 | ))) 115 | .add('single month', withInfo()(() => ( 116 | <DayPicker numberOfMonths={1} /> 117 | ))) 118 | .add('3 months', withInfo()(() => ( 119 | <DayPicker numberOfMonths={3} /> 120 | ))) 121 | .add('vertical', withInfo()(() => ( 122 | <DayPicker 123 | numberOfMonths={2} 124 | orientation={VERTICAL_ORIENTATION} 125 | /> 126 | ))) 127 | .add('vertically scrollable with 12 months', withInfo()(() => ( 128 | <div 129 | style={{ 130 | height: 568, 131 | width: 320, 132 | }} 133 | > 134 | <DayPicker 135 | numberOfMonths={12} 136 | orientation={VERTICAL_SCROLLABLE} 137 | /> 138 | </div> 139 | ))) 140 | .add('vertical with custom day size', withInfo()(() => ( 141 | <DayPicker 142 | numberOfMonths={2} 143 | orientation={VERTICAL_ORIENTATION} 144 | daySize={50} 145 | /> 146 | ))) 147 | .add('vertical with custom height', withInfo()(() => ( 148 | <DayPicker 149 | numberOfMonths={2} 150 | orientation={VERTICAL_ORIENTATION} 151 | verticalHeight={568} 152 | /> 153 | ))) 154 | .add('vertical with DirectionProvider', withInfo()(() => ( 155 | <DirectionProvider direction={DIRECTIONS.RTL}> 156 | <DayPicker 157 | numberOfMonths={2} 158 | orientation={VERTICAL_ORIENTATION} 159 | isRTL 160 | /> 161 | </DirectionProvider> 162 | ))) 163 | .add('vertically scrollable with DirectionProvider', withInfo()(() => ( 164 | <DirectionProvider direction={DIRECTIONS.RTL}> 165 | <div 166 | style={{ 167 | height: 568, 168 | width: 320, 169 | }} 170 | > 171 | <DayPicker 172 | numberOfMonths={12} 173 | orientation={VERTICAL_SCROLLABLE} 174 | /> 175 | </div> 176 | </DirectionProvider> 177 | ))) 178 | .add('with custom arrows', withInfo()(() => ( 179 | <DayPicker 180 | navPrev={<TestPrevIcon />} 181 | navNext={<TestNextIcon />} 182 | /> 183 | ))) 184 | .add('with custom navigation buttons', withInfo()(() => ( 185 | <DayPicker 186 | renderNavPrevButton={renderNavPrevButton} 187 | renderNavNextButton={renderNavNextButton} 188 | /> 189 | ))) 190 | .add('with custom details', withInfo()(() => ( 191 | <DayPicker 192 | renderDayContents={(day) => (day.day() % 6 === 5 ? '😻' : day.format('D'))} 193 | /> 194 | ))) 195 | .add('vertical with fixed-width container', withInfo()(() => ( 196 | <div style={{ width: '400px' }}> 197 | <DayPicker 198 | numberOfMonths={2} 199 | orientation={VERTICAL_ORIENTATION} 200 | /> 201 | </div> 202 | ))) 203 | .add('with info panel', withInfo()(() => ( 204 | <DayPicker 205 | renderCalendarInfo={() => ( 206 | <TestCustomInfoPanel /> 207 | )} 208 | /> 209 | ))) 210 | .add('with custom week header text', withInfo()(() => ( 211 | <DayPicker 212 | renderWeekHeaderElement={(day) => ( 213 | <strong style={{ color: '#FE01E5' }}><small>{day.toUpperCase()}</small></strong> 214 | )} 215 | /> 216 | ))) 217 | .add('with custom week day format', withInfo()(() => ( 218 | <DayPicker 219 | weekDayFormat="ddd" 220 | /> 221 | ))) 222 | .add('with no animation', withInfo()(() => ( 223 | <DayPicker 224 | transitionDuration={0} 225 | /> 226 | ))) 227 | .add('noBorder', withInfo()(() => ( 228 | <DayPicker noBorder /> 229 | ))); 230 | -------------------------------------------------------------------------------- /stories/InfoPanelDecorator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export function monospace(text) { 5 | return `<span style="font-family:monospace;background:#f7f7f7">${text}</span>`; 6 | } 7 | 8 | function InfoPanel({ text }) { 9 | return ( 10 | <div 11 | style={{ 12 | backgroundColor: '#fff', 13 | fontColor: '#3c3f40', 14 | fontSize: 14, 15 | margin: '8px 0', 16 | padding: 16, 17 | }} 18 | > 19 | <span dangerouslySetInnerHTML={{ __html: text }} /> 20 | </div> 21 | ); 22 | } 23 | 24 | InfoPanel.propTypes = { 25 | text: PropTypes.string.isRequired, 26 | }; 27 | 28 | export default function InfoPanelDecorator(text) { 29 | return story => ( 30 | <div> 31 | <InfoPanel text={text} /> 32 | {story()} 33 | </div> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /stories/PresetDateRangePicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { withInfo } from '@storybook/addon-info'; 4 | import moment from 'moment'; 5 | 6 | import PresetDateRangePicker from '../examples/PresetDateRangePicker'; 7 | 8 | import InfoPanelDecorator, { monospace } from './InfoPanelDecorator'; 9 | 10 | const presetDateRangePickerControllerInfo = `The ${monospace('PresetDateRangePicker')} component is not 11 | exported by ${monospace('react-dates')}. It is instead an example of how you might use the 12 | ${monospace('DateRangePicker')} along with the ${monospace('renderCalendarInfo')} prop in 13 | order to add preset range buttons for easy range selection. You can see the example code 14 | <a href="https://github.com/react-dates/react-dates/blob/HEAD/examples/PresetDateRangePicker.jsx"> 15 | here</a> and 16 | <a href="https://github.com/react-dates/react-dates/blob/HEAD/stories/PresetDateRangePicker.js"> 17 | here</a>.`; 18 | 19 | const today = moment(); 20 | const tomorrow = moment().add(1, 'day'); 21 | const presets = [{ 22 | text: 'Today', 23 | start: today, 24 | end: today, 25 | }, 26 | { 27 | text: 'Tomorrow', 28 | start: tomorrow, 29 | end: tomorrow, 30 | }, 31 | { 32 | text: 'Next Week', 33 | start: today, 34 | end: moment().add(1, 'week'), 35 | }, 36 | { 37 | text: 'Next Month', 38 | start: today, 39 | end: moment().add(1, 'month'), 40 | }]; 41 | 42 | storiesOf('PresetDateRangePicker', module) 43 | .addDecorator(InfoPanelDecorator(presetDateRangePickerControllerInfo)) 44 | .add('default', withInfo()(() => ( 45 | <PresetDateRangePicker 46 | presets={presets} 47 | autoFocus 48 | /> 49 | ))); 50 | -------------------------------------------------------------------------------- /stories/SingleDatePicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import momentJalaali from 'moment-jalaali'; 4 | import { storiesOf } from '@storybook/react'; 5 | import { withInfo } from '@storybook/addon-info'; 6 | import DirectionProvider, { DIRECTIONS } from 'react-with-direction/dist/DirectionProvider'; 7 | import isInclusivelyBeforeDay from '../src/utils/isInclusivelyBeforeDay'; 8 | import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; 9 | 10 | import { 11 | VERTICAL_ORIENTATION, 12 | ANCHOR_RIGHT, 13 | } from '../src/constants'; 14 | 15 | import SingleDatePickerWrapper from '../examples/SingleDatePickerWrapper'; 16 | 17 | const TestInput = props => ( 18 | <div style={{ marginTop: 16 }} > 19 | <input 20 | {...props} 21 | type="text" 22 | style={{ 23 | height: 48, 24 | width: 284, 25 | fontSize: 18, 26 | fontWeight: 200, 27 | padding: '12px 16px', 28 | }} 29 | /> 30 | </div> 31 | ); 32 | 33 | storiesOf('SingleDatePicker (SDP)', module) 34 | .add('default', withInfo()(() => ( 35 | <SingleDatePickerWrapper /> 36 | ))) 37 | .add('as part of a form', withInfo()(() => ( 38 | <div> 39 | <SingleDatePickerWrapper /> 40 | <TestInput placeholder="Input 1" /> 41 | <TestInput placeholder="Input 2" /> 42 | <TestInput placeholder="Input 3" /> 43 | </div> 44 | ))) 45 | .add('non-english locale (Chinese)', withInfo()(() => { 46 | moment.locale('zh-cn'); 47 | return ( 48 | <SingleDatePickerWrapper 49 | placeholder="入住日期" 50 | monthFormat="YYYY[年]MMMM" 51 | phrases={{ 52 | closeDatePicker: '关闭', 53 | clearDate: '清除日期', 54 | }} 55 | /> 56 | ); 57 | })) 58 | .add('non-english locale (Persian)', withInfo()(() => { 59 | moment.locale('fa'); 60 | return ( 61 | <SingleDatePickerWrapper 62 | placeholder="تقویم فارسی" 63 | renderMonthText={month => momentJalaali(month).format('jMMMM jYYYY')} 64 | renderDayContents={day => momentJalaali(day).format('jD')} 65 | /> 66 | ); 67 | })) 68 | .add('with DirectionProvider', withInfo()(() => ( 69 | <DirectionProvider direction={DIRECTIONS.RTL}> 70 | <SingleDatePickerWrapper 71 | placeholder="تاریخ شروع" 72 | anchorDirection={ANCHOR_RIGHT} 73 | showDefaultInputIcon 74 | showClearDate 75 | isRTL 76 | /> 77 | </DirectionProvider> 78 | ))) 79 | .add('with custom month navigation and blocked navigation (minDate and maxDate)', withInfo()(() => ( 80 | <SingleDatePickerWrapper 81 | minDate={moment().subtract(2, 'months').startOf('month')} 82 | maxDate={moment().add(2, 'months').endOf('month')} 83 | /> 84 | ))) 85 | .add('with custom isOutsideRange and month navigation and blocked navigation (minDate and maxDate)', withInfo()(() => { 86 | const minDate = moment().subtract(2, 'months').startOf('month') 87 | const maxDate = moment().add(2, 'months').endOf('month') 88 | const isOutsideRange = day => isInclusivelyBeforeDay(day, minDate) || isInclusivelyAfterDay(day, maxDate) 89 | return ( 90 | <SingleDatePickerWrapper 91 | minDate={minDate} 92 | maxDate={maxDate} 93 | isOutsideRange={isOutsideRange} 94 | /> 95 | )})) 96 | .add('vertical with custom height', withInfo()(() => ( 97 | <SingleDatePickerWrapper 98 | orientation={VERTICAL_ORIENTATION} 99 | verticalHeight={568} 100 | /> 101 | ))) 102 | .add('with custom autoComplete attribute', withInfo()(() => ( 103 | <SingleDatePickerWrapper 104 | autoComplete="datePicker" 105 | /> 106 | ))); 107 | -------------------------------------------------------------------------------- /stories/SingleDatePicker_day.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; 7 | import isSameDay from '../src/utils/isSameDay'; 8 | 9 | import SingleDatePickerWrapper from '../examples/SingleDatePickerWrapper'; 10 | 11 | const datesList = [ 12 | moment(), 13 | moment().add(1, 'days'), 14 | moment().add(3, 'days'), 15 | moment().add(9, 'days'), 16 | moment().add(10, 'days'), 17 | moment().add(11, 'days'), 18 | moment().add(12, 'days'), 19 | moment().add(13, 'days'), 20 | ]; 21 | 22 | storiesOf('SDP - Day Props', module) 23 | .add('default', withInfo()(() => ( 24 | <SingleDatePickerWrapper autoFocus /> 25 | ))) 26 | .add('allows all days, including past days', withInfo()(() => ( 27 | <SingleDatePickerWrapper 28 | isOutsideRange={() => false} 29 | autoFocus 30 | /> 31 | ))) 32 | .add('allows next two weeks only', withInfo()(() => ( 33 | <SingleDatePickerWrapper 34 | isOutsideRange={day => 35 | !isInclusivelyAfterDay(day, moment()) || 36 | isInclusivelyAfterDay(day, moment().add(2, 'weeks')) 37 | } 38 | autoFocus 39 | /> 40 | ))) 41 | .add('with some blocked dates', withInfo()(() => ( 42 | <SingleDatePickerWrapper 43 | isDayBlocked={day1 => datesList.some(day2 => isSameDay(day1, day2))} 44 | autoFocus 45 | /> 46 | ))) 47 | .add('with some highlighted dates', withInfo()(() => ( 48 | <SingleDatePickerWrapper 49 | isDayHighlighted={day1 => datesList.some(day2 => isSameDay(day1, day2))} 50 | autoFocus 51 | /> 52 | ))) 53 | .add('blocks fridays', withInfo()(() => ( 54 | <SingleDatePickerWrapper 55 | isDayBlocked={day => moment.weekdays(day.weekday()) === 'Friday'} 56 | autoFocus 57 | /> 58 | ))) 59 | .add('with custom daily details', withInfo()(() => ( 60 | <SingleDatePickerWrapper 61 | numberOfMonths={1} 62 | renderDayContents={day => day.format('ddd')} 63 | autoFocus 64 | /> 65 | ))); 66 | -------------------------------------------------------------------------------- /stories/SingleDatePicker_input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import SingleDatePickerWrapper from '../examples/SingleDatePickerWrapper'; 7 | 8 | const TestCustomInputIcon = () => ( 9 | <span 10 | style={{ 11 | border: '1px solid #dce0e0', 12 | backgroundColor: '#fff', 13 | color: '#484848', 14 | padding: '3px', 15 | }} 16 | > 17 | C 18 | </span> 19 | ); 20 | 21 | storiesOf('SDP - Input Props', module) 22 | .add('default', withInfo()(() => ( 23 | <SingleDatePickerWrapper 24 | initialDate={moment().add(3, 'days')} 25 | /> 26 | ))) 27 | .add('disabled', withInfo()(() => ( 28 | <SingleDatePickerWrapper 29 | initialDate={moment().add(3, 'days')} 30 | disabled 31 | /> 32 | ))) 33 | .add('readOnly', withInfo()(() => ( 34 | <SingleDatePickerWrapper 35 | initialDate={moment().add(3, 'days')} 36 | readOnly 37 | /> 38 | ))) 39 | .add('with clear dates button', withInfo()(() => ( 40 | <SingleDatePickerWrapper 41 | initialDate={moment().add(3, 'days')} 42 | showClearDate 43 | /> 44 | ))) 45 | .add('reopens DayPicker on clear dates', withInfo()(() => ( 46 | <SingleDatePickerWrapper 47 | initialDate={moment().add(3, 'days')} 48 | showClearDate 49 | reopenPickerOnClearDate 50 | /> 51 | ))) 52 | .add('with custom display format', withInfo()(() => ( 53 | <SingleDatePickerWrapper 54 | initialDate={moment().add(3, 'days')} 55 | displayFormat="MMM D" 56 | /> 57 | ))) 58 | .add('with show calendar icon', withInfo()(() => ( 59 | <SingleDatePickerWrapper 60 | initialDate={moment().add(3, 'days')} 61 | showDefaultInputIcon 62 | /> 63 | ))) 64 | .add('with custom show calendar icon', withInfo()(() => ( 65 | <SingleDatePickerWrapper 66 | initialDate={moment().add(3, 'days')} 67 | customInputIcon={<TestCustomInputIcon />} 68 | /> 69 | ))) 70 | .add('with show calendar icon after input', withInfo()(() => ( 71 | <SingleDatePickerWrapper 72 | initialDate={moment().add(3, 'days')} 73 | showDefaultInputIcon 74 | inputIconPosition="after" 75 | /> 76 | ))) 77 | .add('with screen reader message', withInfo()(() => ( 78 | <SingleDatePickerWrapper 79 | initialDate={moment().add(3, 'days')} 80 | screenReaderInputMessage="Here you could inform screen reader users of the date format, minimum nights, blocked out dates, etc" 81 | /> 82 | ))) 83 | .add('with custom title attribute', withInfo()(() => ( 84 | <SingleDatePickerWrapper 85 | initialDate={moment().add(3, 'days')} 86 | titleText="Here you can set the title attribute of the input, which shows in the tooltip on :hover over the field" 87 | /> 88 | ))) 89 | .add('noBorder', withInfo()(() => ( 90 | <SingleDatePickerWrapper 91 | initialDate={moment().add(3, 'days')} 92 | noBorder 93 | /> 94 | ))) 95 | .add('block styling', withInfo()(() => ( 96 | <SingleDatePickerWrapper 97 | initialDate={moment().add(3, 'days')} 98 | showClearDate 99 | block 100 | /> 101 | ))) 102 | .add('small styling', withInfo()(() => ( 103 | <SingleDatePickerWrapper 104 | initialDate={moment().add(3, 'days')} 105 | showClearDate 106 | small 107 | /> 108 | ))) 109 | .add('regular styling', withInfo()(() => ( 110 | <SingleDatePickerWrapper 111 | initialDate={moment().add(3, 'days')} 112 | showClearDate 113 | regular 114 | /> 115 | ))); 116 | -------------------------------------------------------------------------------- /test/_helpers/describeIfWindow.js: -------------------------------------------------------------------------------- 1 | export default typeof document === 'undefined' ? describe.skip : describe; 2 | -------------------------------------------------------------------------------- /test/_helpers/enzymeSetup.js: -------------------------------------------------------------------------------- 1 | import configure from 'enzyme-adapter-react-helper'; 2 | 3 | configure({ disableLifecycleMethods: true }); 4 | -------------------------------------------------------------------------------- /test/_helpers/registerReactWithStylesInterface.js: -------------------------------------------------------------------------------- 1 | import ThemedStyleSheet from 'react-with-styles/lib/ThemedStyleSheet'; 2 | import aphroditeInterface from 'react-with-styles-interface-aphrodite'; 3 | import { StyleSheetTestUtils } from 'aphrodite'; 4 | 5 | import DefaultTheme from '../../src/theme/DefaultTheme'; 6 | 7 | ThemedStyleSheet.registerTheme(DefaultTheme); 8 | ThemedStyleSheet.registerInterface(aphroditeInterface); 9 | 10 | beforeEach(() => { 11 | StyleSheetTestUtils.suppressStyleInjection(); 12 | }); 13 | 14 | afterEach(() => { 15 | StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); 16 | }); 17 | -------------------------------------------------------------------------------- /test/_helpers/restoreSinonStubs.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon-sandbox'; 2 | 3 | afterEach(() => { 4 | sinon.restore(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/_helpers/withTouchSupport.js: -------------------------------------------------------------------------------- 1 | const wrap = require('mocha-wrap'); 2 | const withGlobal = require('mocha-wrap/withGlobal'); 3 | const withOverride = require('mocha-wrap/withOverride'); 4 | 5 | function withTouchSupport() { 6 | return this 7 | .use(withGlobal, 'window', () => (typeof window !== 'undefined' ? window : {})) 8 | .use(withOverride, () => window, 'ontouchstart', () => window.ontouchstart || (() => {})) 9 | .use(withGlobal, 'navigator', () => (typeof navigator !== 'undefined' ? navigator : {})) 10 | .use(withOverride, () => navigator, 'maxTouchPoints', () => navigator.maxTouchPoints || true); 11 | } 12 | 13 | wrap.register(withTouchSupport); 14 | -------------------------------------------------------------------------------- /test/browser-main.js: -------------------------------------------------------------------------------- 1 | const requireAll = (requireContext) => requireContext.keys().forEach(requireContext); 2 | 3 | if (typeof window !== 'undefined') { 4 | requireAll(require.context('./_helpers', true, /.jsx?$/)); 5 | requireAll(require.context('.', true, /.jsx?$/)); 6 | } 7 | -------------------------------------------------------------------------------- /test/components/CalendarMonthGrid_spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | import moment from 'moment'; 5 | import sinon from 'sinon-sandbox'; 6 | 7 | import CalendarMonth from '../../src/components/CalendarMonth'; 8 | import CalendarMonthGrid from '../../src/components/CalendarMonthGrid'; 9 | 10 | import getTransformStyles from '../../src/utils/getTransformStyles'; 11 | 12 | describe('CalendarMonthGrid', () => { 13 | it('the number of CalendarMonths rendered matches props.numberOfMonths + 2', () => { 14 | const NUM_OF_MONTHS = 5; 15 | const wrapper = shallow(<CalendarMonthGrid numberOfMonths={NUM_OF_MONTHS} />).dive(); 16 | expect(wrapper.find(CalendarMonth)).to.have.lengthOf(NUM_OF_MONTHS + 2); 17 | }); 18 | 19 | it('has style equal to getTransformStyles(foo)', () => { 20 | const translationValue = 100; 21 | const transformStyles = getTransformStyles(`translateX(${translationValue}px)`); 22 | const wrapper = shallow(<CalendarMonthGrid translationValue={translationValue} />).dive(); 23 | Object.keys(transformStyles).forEach((key) => { 24 | expect(wrapper.prop('style')[key]).to.equal(transformStyles[key]); 25 | }); 26 | }); 27 | 28 | it('does not generate duplicate months', () => { 29 | const initialMonth = moment(); 30 | const wrapper = shallow(( 31 | <CalendarMonthGrid numberOfMonths={12} initialMonth={initialMonth} /> 32 | )).dive(); 33 | 34 | wrapper.instance().componentWillReceiveProps({ 35 | initialMonth, 36 | numberOfMonths: 24, 37 | }); 38 | 39 | const { months } = wrapper.state(); 40 | 41 | const collisions = months 42 | .map((m) => m.format('YYYY-MM')) 43 | .reduce((acc, m) => ({ ...acc, [m]: true }), {}); 44 | 45 | expect(Object.keys(collisions).length).to.equal(months.length); 46 | }); 47 | 48 | it('does not setState if hasMonthChanged and hasNumberOfMonthsChanged are falsy', () => { 49 | const setState = sinon.stub(CalendarMonthGrid.prototype, 'setState'); 50 | const initialMonth = moment(); 51 | const wrapper = shallow(( 52 | <CalendarMonthGrid numberOfMonths={12} initialMonth={initialMonth} /> 53 | )).dive(); 54 | 55 | wrapper.instance().componentWillReceiveProps({ 56 | initialMonth, 57 | numberOfMonths: 12, 58 | }); 59 | 60 | expect(setState.callCount).to.eq(0); 61 | }); 62 | 63 | it('works with the same number of months', () => { 64 | const initialMonth = moment(); 65 | const wrapper = shallow(( 66 | <CalendarMonthGrid numberOfMonths={12} initialMonth={initialMonth} /> 67 | )).dive(); 68 | 69 | wrapper.instance().componentWillReceiveProps({ 70 | initialMonth, 71 | numberOfMonths: 12, 72 | firstVisibleMonthIndex: 0, 73 | }); 74 | 75 | const { months } = wrapper.state(); 76 | 77 | const collisions = months 78 | .map((m) => m.format('YYYY-MM')) 79 | .reduce((acc, m) => ({ ...acc, [m]: true }), {}); 80 | 81 | expect(Object.keys(collisions).length).to.equal(months.length); 82 | }); 83 | 84 | describe('#onMonthSelect', () => { 85 | it('calls onMonthChange', () => { 86 | const onMonthChangeSpy = sinon.spy(); 87 | const wrapper = shallow(<CalendarMonthGrid onMonthChange={onMonthChangeSpy} />).dive(); 88 | const currentMonth = moment(); 89 | const newMonthVal = (currentMonth.month() + 5) % 12; 90 | wrapper.instance().onMonthSelect(currentMonth, newMonthVal); 91 | expect(onMonthChangeSpy.callCount).to.equal(1); 92 | }); 93 | }); 94 | 95 | describe('#onYearSelect', () => { 96 | it('calls onYearChange', () => { 97 | const onYearChangeSpy = sinon.spy(); 98 | const wrapper = shallow(<CalendarMonthGrid onYearChange={onYearChangeSpy} />).dive(); 99 | const currentMonth = moment(); 100 | const newMonthVal = (currentMonth.month() + 5) % 12; 101 | wrapper.instance().onYearSelect(currentMonth, newMonthVal); 102 | expect(onYearChangeSpy.callCount).to.equal(1); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/components/CalendarMonth_spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow, mount } from 'enzyme'; 4 | import sinon from 'sinon-sandbox'; 5 | import moment from 'moment'; 6 | import describeIfWindow from '../_helpers/describeIfWindow'; 7 | 8 | import CalendarMonth from '../../src/components/CalendarMonth'; 9 | 10 | describe('CalendarMonth', () => { 11 | describe('#render', () => { 12 | describe('data-visible attribute', () => { 13 | it('data-visible attribute is truthy if props.isVisible', () => { 14 | const wrapper = shallow(<CalendarMonth isVisible />).dive(); 15 | expect(wrapper.prop('data-visible')).to.equal(true); 16 | }); 17 | 18 | it('data-visible attribute is falsy if !props.isVisible', () => { 19 | const wrapper = shallow(<CalendarMonth isVisible={false} />).dive(); 20 | expect(wrapper.prop('data-visible')).to.equal(false); 21 | }); 22 | }); 23 | 24 | describe('caption', () => { 25 | it('text is the correctly formatted month title', () => { 26 | const MONTH = moment(); 27 | const captionWrapper = shallow(<CalendarMonth month={MONTH} />).dive().find('strong'); 28 | expect(captionWrapper.text()).to.equal(MONTH.format('MMMM YYYY')); 29 | }); 30 | }); 31 | 32 | it('renderMonthElement renders month element when month changes', () => { 33 | const renderMonthElementStub = sinon.stub().returns(<div id="month-element" />); 34 | const wrapper = shallow(<CalendarMonth renderMonthElement={renderMonthElementStub} />).dive(); 35 | wrapper.setProps({ month: moment().subtract(1, 'months') }); 36 | 37 | const [{ 38 | month, 39 | onMonthSelect, 40 | onYearSelect, 41 | isVisible, 42 | }] = renderMonthElementStub.getCall(0).args; 43 | 44 | expect(moment.isMoment(month)).to.equal(true); 45 | expect(typeof onMonthSelect).to.equal('function'); 46 | expect(typeof onYearSelect).to.equal('function'); 47 | expect(typeof isVisible).to.equal('boolean'); 48 | expect(wrapper.find('#month-element').exists()).to.equal(true); 49 | }); 50 | 51 | describeIfWindow('setMonthTitleHeight', () => { 52 | beforeEach(() => { 53 | sinon.stub(window, 'setTimeout').callsFake((handler) => handler()); 54 | }); 55 | 56 | it('sets the title height after mount', () => { 57 | const setMonthTitleHeightStub = sinon.stub(); 58 | mount( 59 | <CalendarMonth 60 | isVisible 61 | setMonthTitleHeight={setMonthTitleHeightStub} 62 | />, 63 | ); 64 | 65 | expect(setMonthTitleHeightStub).to.have.property('callCount', 1); 66 | }); 67 | 68 | describe('if the callbacks gets set again', () => { 69 | it('updates the title height', () => { 70 | const setMonthTitleHeightStub = sinon.stub(); 71 | const wrapper = mount( 72 | <CalendarMonth 73 | isVisible 74 | setMonthTitleHeight={setMonthTitleHeightStub} 75 | />, 76 | ); 77 | 78 | expect(setMonthTitleHeightStub).to.have.property('callCount', 1); 79 | 80 | wrapper.setProps({ setMonthTitleHeight: null }); 81 | 82 | wrapper.setProps({ setMonthTitleHeight: setMonthTitleHeightStub }); 83 | expect(setMonthTitleHeightStub).to.have.property('callCount', 2); 84 | }); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/components/CalendarWeek_spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import CalendarWeek from '../../src/components/CalendarWeek'; 6 | 7 | import CalendarDay from '../../src/components/CalendarDay'; 8 | 9 | describe('CalendarWeek', () => { 10 | it('renders a tr', () => { 11 | const wrapper = shallow(( 12 | <CalendarWeek> 13 | <CalendarDay /> 14 | </CalendarWeek> 15 | )); 16 | expect(wrapper.is('tr')).to.equal(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/components/KeyboardShortcutRow_spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import KeyboardShortcutRow from '../../src/components/KeyboardShortcutRow'; 6 | 7 | describe('KeyboardShortcutRow', () => { 8 | it('is an li', () => { 9 | const wrapper = shallow(( 10 | <KeyboardShortcutRow 11 | unicode="foo" 12 | label="bar" 13 | action="baz" 14 | /> 15 | )).dive(); 16 | expect(wrapper.is('li')).to.equal(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/components/SingleDatePickerInput_spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | import sinon from 'sinon-sandbox'; 5 | 6 | import SingleDatePickerInput from '../../src/components/SingleDatePickerInput'; 7 | import DateInput from '../../src/components/DateInput'; 8 | import { SingleDatePickerInputPhrases } from '../../src/defaultPhrases'; 9 | 10 | describe('SingleDatePickerInput', () => { 11 | describe('render', () => { 12 | it('should render any children provided', () => { 13 | const Child = () => <div>CHILD</div>; 14 | 15 | const wrapper = shallow(( 16 | <SingleDatePickerInput id="date"> 17 | <Child /> 18 | </SingleDatePickerInput> 19 | )).dive(); 20 | expect(wrapper.find(Child)).to.have.lengthOf(1); 21 | }); 22 | }); 23 | 24 | describe('clear date', () => { 25 | describe('props.showClearDate is falsy', () => { 26 | it('does not render a clear date button', () => { 27 | const wrapper = shallow(<SingleDatePickerInput id="date" showClearDate={false} />).dive(); 28 | expect(wrapper.find('button')).to.have.lengthOf(0); 29 | }); 30 | }); 31 | 32 | describe('props.showClearDate is truthy', () => { 33 | it('has a clear date button', () => { 34 | const wrapper = shallow(<SingleDatePickerInput id="date" showClearDate />).dive(); 35 | expect(wrapper.find('button')).to.have.lengthOf(1); 36 | }); 37 | }); 38 | 39 | describe('props.customCloseIcon is a React Element', () => { 40 | it('has custom icon', () => { 41 | const wrapper = shallow(( 42 | <SingleDatePickerInput 43 | id="date" 44 | showClearDate 45 | customCloseIcon={<span className="custom-close-icon" />} 46 | /> 47 | )).dive(); 48 | expect(wrapper.find('.custom-close-icon')).to.have.lengthOf(1); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('show calendar icon', () => { 54 | describe('props.showInputIcon is falsy', () => { 55 | it('does not have a calendar button', () => { 56 | const wrapper = shallow(( 57 | <SingleDatePickerInput id="date" showDefaultInputIcon={false} /> 58 | )).dive(); 59 | expect(wrapper.find('button')).to.have.lengthOf(0); 60 | }); 61 | }); 62 | 63 | describe('props.showInputIcon is truthy', () => { 64 | it('has button', () => { 65 | const wrapper = shallow(<SingleDatePickerInput id="date" showDefaultInputIcon />).dive(); 66 | expect(wrapper.find('button')).to.have.lengthOf(1); 67 | }); 68 | }); 69 | 70 | describe('props.customInputIcon is a React Element', () => { 71 | it('has custom icon', () => { 72 | const wrapper = shallow(( 73 | <SingleDatePickerInput 74 | id="date" 75 | customInputIcon={<span className="custom-icon" />} 76 | /> 77 | )).dive(); 78 | expect(wrapper.find('.custom-icon')).to.have.lengthOf(1); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('clear date interactions', () => { 84 | describe('onClick', () => { 85 | it('props.onClearDate gets triggered', () => { 86 | const onClearDateSpy = sinon.spy(); 87 | const wrapper = shallow(( 88 | <SingleDatePickerInput 89 | id="date" 90 | onClearDate={onClearDateSpy} 91 | showClearDate 92 | /> 93 | )).dive(); 94 | const clearDateWrapper = wrapper.find('button'); 95 | clearDateWrapper.simulate('click'); 96 | expect(onClearDateSpy).to.have.property('called', true); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('screen reader message', () => { 102 | describe('props.screenReaderMessage is falsy', () => { 103 | it('default value is passed to DateInput', () => { 104 | const wrapper = shallow(<SingleDatePickerInput id="date" />).dive(); 105 | const dateInput = wrapper.find(DateInput); 106 | expect(dateInput).to.have.lengthOf(1); 107 | expect(dateInput.props()).to.have.property( 108 | 'screenReaderMessage', 109 | SingleDatePickerInputPhrases.keyboardForwardNavigationInstructions, 110 | ); 111 | }); 112 | }); 113 | 114 | describe('props.screenReaderMessage is truthy', () => { 115 | it('prop value is passed to DateInput', () => { 116 | const message = 'test message'; 117 | const wrapper = shallow(( 118 | <SingleDatePickerInput 119 | id="date" 120 | screenReaderMessage={message} 121 | /> 122 | )).dive(); 123 | const dateInput = wrapper.find(DateInput); 124 | expect(dateInput).to.have.lengthOf(1); 125 | expect(dateInput.props()).to.have.property('screenReaderMessage', message); 126 | }); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require airbnb-js-shims 2 | test-build/**/*.js 3 | -------------------------------------------------------------------------------- /test/utils/calculateDimension_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import calculateDimension from '../../src/utils/calculateDimension'; 4 | 5 | describe('#calculateDimension', () => { 6 | it('returns 0 for an empty element', () => { 7 | expect(calculateDimension(null, 'width')).to.equal(0); 8 | expect(calculateDimension(null, 'width', false)).to.equal(0); 9 | expect(calculateDimension(null, 'width', true)).to.equal(0); 10 | }); 11 | 12 | describe('borderBox true', () => { 13 | const el = { 14 | offsetWidth: 17, 15 | offsetHeight: 42, 16 | }; 17 | 18 | it('returns el.offsetWidth for "width"', () => { 19 | expect(calculateDimension(el, 'width', true)).to.equal(el.offsetWidth); 20 | }); 21 | 22 | it('returns el.offsetHeight for "height"', () => { 23 | expect(calculateDimension(el, 'height', true)).to.equal(el.offsetHeight); 24 | }); 25 | }); 26 | 27 | /* Requires a DOM */ 28 | describe.skip('withMargin false and borderBox true', () => { 29 | let testElement = null; 30 | 31 | beforeEach(() => { 32 | testElement = document.createElement('div'); 33 | 34 | testElement.style.width = '100px'; 35 | testElement.style.height = '250px'; 36 | testElement.style.padding = '15px 10px'; 37 | testElement.style.border = '1px solid red'; 38 | testElement.style.margin = '3px 6px 5px 2px'; 39 | testElement.boxSizing = 'border-box'; 40 | }); 41 | 42 | it('calculates border-box height', () => { 43 | expect(calculateDimension(testElement, 'height', true)).to.equal(282); 44 | }); 45 | 46 | it('calculates border-box height with margin', () => { 47 | expect(calculateDimension(testElement, 'height', true, true)).to.equal(290); 48 | }); 49 | 50 | it('calculates border-box width', () => { 51 | expect(calculateDimension(testElement, 'width', true)).to.equal(122); 52 | }); 53 | 54 | it('calculates border-box width with margin', () => { 55 | expect(calculateDimension(testElement, 'width', true, true)).to.equal(130); 56 | }); 57 | 58 | it('calculates content-box height', () => { 59 | expect(calculateDimension(testElement, 'height')).to.equal(250); 60 | }); 61 | 62 | it('calculates content-box height with margin', () => { 63 | expect(calculateDimension(testElement, 'height', false, true)).to.equal(258); 64 | }); 65 | 66 | it('calculates content-box width', () => { 67 | expect(calculateDimension(testElement, 'width')).to.equal(100); 68 | }); 69 | 70 | it('calculates content-box width with margin', () => { 71 | expect(calculateDimension(testElement, 'width', false, true)).to.equal(108); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/utils/disableScroll_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import disableScroll, { 4 | getScrollParent, 5 | getScrollAncestorsOverflowY, 6 | } from '../../src/utils/disableScroll'; 7 | 8 | import describeIfWindow from '../_helpers/describeIfWindow'; 9 | 10 | const createScrollContainer = (level = 1) => { 11 | const el = document.createElement('div'); 12 | el.style.width = '300px'; 13 | el.style.height = `${level * 100}px`; 14 | el.style.overflow = 'auto'; 15 | return el; 16 | }; 17 | 18 | const createScrollContent = () => { 19 | const el = document.createElement('div'); 20 | el.style.width = '100%'; 21 | el.style.height = '99999px'; 22 | return el; 23 | }; 24 | 25 | const createTargetElement = () => { 26 | const el = document.createElement('div'); 27 | return el; 28 | }; 29 | 30 | describeIfWindow('#disableScroll', () => { 31 | let grandParentScrollContainer; 32 | let parentScrollContainer; 33 | let scrollContent; 34 | let targetElement; 35 | 36 | before(() => { 37 | grandParentScrollContainer = createScrollContainer(1); 38 | parentScrollContainer = createScrollContainer(2); 39 | scrollContent = createScrollContent(); 40 | targetElement = createTargetElement(); 41 | scrollContent.appendChild(targetElement); 42 | }); 43 | 44 | describe('#getScrollParent', () => { 45 | describe('with no parent', () => { 46 | before(() => { 47 | document.body.appendChild(scrollContent); 48 | }); 49 | 50 | after(() => { 51 | document.body.removeChild(scrollContent); 52 | }); 53 | 54 | it('returns scrolling root if no scroll parent', () => { 55 | const scrollParent = getScrollParent(targetElement); 56 | expect(scrollParent).to.equal(document.documentElement); 57 | }); 58 | }); 59 | 60 | describe('with a single scroll container', () => { 61 | before(() => { 62 | parentScrollContainer.appendChild(scrollContent); 63 | document.body.appendChild(parentScrollContainer); 64 | }); 65 | 66 | after(() => { 67 | parentScrollContainer.appendChild(scrollContent); 68 | document.body.removeChild(parentScrollContainer); 69 | }); 70 | 71 | it('returns the scroll container', () => { 72 | const scrollParent = getScrollParent(targetElement); 73 | expect(scrollParent).to.equal(parentScrollContainer); 74 | }); 75 | }); 76 | 77 | describe('with multiple scroll containers', () => { 78 | before(() => { 79 | parentScrollContainer.appendChild(scrollContent); 80 | grandParentScrollContainer.appendChild(parentScrollContainer); 81 | document.body.appendChild(grandParentScrollContainer); 82 | }); 83 | 84 | after(() => { 85 | parentScrollContainer.removeChild(scrollContent); 86 | grandParentScrollContainer.removeChild(parentScrollContainer); 87 | document.body.removeChild(grandParentScrollContainer); 88 | }); 89 | 90 | it('returns the closest scroll container', () => { 91 | const scrollParent = getScrollParent(targetElement); 92 | expect(scrollParent).to.equal(parentScrollContainer); 93 | }); 94 | }); 95 | }); 96 | 97 | describe('#getScrollAncestorsOverflowY', () => { 98 | before(() => { 99 | parentScrollContainer.appendChild(scrollContent); 100 | grandParentScrollContainer.appendChild(parentScrollContainer); 101 | document.body.appendChild(grandParentScrollContainer); 102 | }); 103 | 104 | after(() => { 105 | parentScrollContainer.removeChild(scrollContent); 106 | grandParentScrollContainer.removeChild(parentScrollContainer); 107 | document.body.removeChild(grandParentScrollContainer); 108 | }); 109 | 110 | it('returns a map with the overflowY of all scrollable ancestors', () => { 111 | const scrollAncestorsOverflowY = getScrollAncestorsOverflowY(targetElement); 112 | expect(scrollAncestorsOverflowY.size).to.equal(3); // both scroll containers + root 113 | 114 | expect(scrollAncestorsOverflowY.has(parentScrollContainer)).to.equal(true); 115 | expect(scrollAncestorsOverflowY.has(grandParentScrollContainer)).to.equal(true); 116 | expect(scrollAncestorsOverflowY.has(document.documentElement)).to.equal(true); 117 | 118 | expect(scrollAncestorsOverflowY.get(parentScrollContainer)).to.equal('auto'); 119 | expect(scrollAncestorsOverflowY.get(grandParentScrollContainer)).to.equal('auto'); 120 | expect(scrollAncestorsOverflowY.get(document.documentElement)).to.be.a('string'); 121 | }); 122 | }); 123 | 124 | describe('#disableScroll', () => { 125 | before(() => { 126 | parentScrollContainer.appendChild(scrollContent); 127 | grandParentScrollContainer.appendChild(parentScrollContainer); 128 | document.body.appendChild(grandParentScrollContainer); 129 | }); 130 | 131 | after(() => { 132 | parentScrollContainer.removeChild(scrollContent); 133 | grandParentScrollContainer.removeChild(parentScrollContainer); 134 | document.body.removeChild(grandParentScrollContainer); 135 | }); 136 | 137 | describe('disable', () => { 138 | it('should set all scroll ancestors overflow to hidden', () => { 139 | const enableScroll = disableScroll(targetElement); 140 | const scrollAncestorsOverflowY = getScrollAncestorsOverflowY(targetElement); 141 | 142 | // eslint-disable-next-line no-restricted-syntax 143 | for (const [, overflowY] of scrollAncestorsOverflowY) { 144 | expect(overflowY).to.equal('hidden'); 145 | } 146 | 147 | // reset to initial state 148 | enableScroll(); 149 | }); 150 | }); 151 | 152 | describe('enable', () => { 153 | it('should set all scroll ancestors overflow to their previous value', () => { 154 | const scrollAncestorsOverflowYBefore = getScrollAncestorsOverflowY(targetElement); 155 | const enableScroll = disableScroll(targetElement); 156 | enableScroll(); 157 | const scrollAncestorsOverflowY = getScrollAncestorsOverflowY(targetElement); 158 | 159 | // eslint-disable-next-line no-restricted-syntax 160 | for (const [element, overflowY] of scrollAncestorsOverflowY) { 161 | expect(scrollAncestorsOverflowYBefore.get(element)).to.equal(overflowY); 162 | } 163 | }); 164 | }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /test/utils/getActiveElement_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import wrap from 'mocha-wrap'; 3 | 4 | import getActiveElement from '../../src/utils/getActiveElement'; 5 | 6 | const describeIfNoWindow = typeof document === 'undefined' ? describe : describe.skip; 7 | const test = 'FOOBARBAZ'; 8 | 9 | describeIfNoWindow('getActiveElement', () => { 10 | describe('without `document`', () => { 11 | it('returns false', () => { 12 | expect(typeof document).to.equal('undefined'); 13 | expect(getActiveElement()).to.equal(false); 14 | }); 15 | }); 16 | 17 | wrap() 18 | .withGlobal('document', () => ({})) 19 | .describe('with `document`', () => { 20 | it('returns undefined without `document.activeElement`', () => { 21 | expect(getActiveElement()).to.be.an('undefined'); 22 | }); 23 | 24 | wrap() 25 | .withOverride(() => document, 'activeElement', () => test) 26 | .it('returns activeElement value with `document.activeElement', () => { 27 | expect(getActiveElement()).to.equal(test); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/utils/getCalendarMonthWidth_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import getCalendarMonthWidth from '../../src/utils/getCalendarMonthWidth'; 4 | 5 | describe('#getCalendarMonthWidth', () => { 6 | it('correctly calculates width for default day size of 39', () => { 7 | expect(getCalendarMonthWidth(39, 13)).to.equal(300); 8 | }); 9 | 10 | it('returns a number when padding is undefined', () => { 11 | expect(Number.isNaN(getCalendarMonthWidth(39, undefined))).to.equal(false); 12 | }); 13 | 14 | it('returns a number when padding is null', () => { 15 | expect(Number.isNaN(getCalendarMonthWidth(39, null))).to.equal(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/utils/getDetachedContainerStyles_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import wrap from 'mocha-wrap'; 3 | 4 | import getDetachedContainerStyles from '../../src/utils/getDetachedContainerStyles'; 5 | 6 | import { 7 | OPEN_DOWN, 8 | OPEN_UP, 9 | ANCHOR_LEFT, 10 | ANCHOR_RIGHT, 11 | } from '../../src/constants'; 12 | 13 | const describeIfNoWindow = typeof document === 'undefined' ? describe : describe.skip; 14 | 15 | const windowWidth = 100; 16 | const windowHeight = 100; 17 | 18 | // Fake 30x100px element on x,y = 10,10 19 | const referenceElRect = { 20 | top: 10, 21 | bottom: 40, 22 | left: 10, 23 | right: 110, 24 | }; 25 | const referenceEl = { 26 | getBoundingClientRect() { 27 | return referenceElRect; 28 | }, 29 | }; 30 | 31 | describeIfNoWindow('#getDetachedContainerStyles', () => { 32 | wrap() 33 | .withGlobal('window', () => ({})) 34 | .withOverride(() => window, 'innerWidth', () => windowWidth) 35 | .withOverride(() => window, 'innerHeight', () => windowHeight) 36 | .describe('with `window`', () => { 37 | describe('on down-left positioning', () => { 38 | it('returns translation from top-left of window to top-left of reference el', () => { 39 | const styles = getDetachedContainerStyles(OPEN_DOWN, ANCHOR_LEFT, referenceEl); 40 | expect(styles.transform).to.equal(`translate3d(${Math.round(referenceElRect.left)}px, ${Math.round(referenceElRect.top)}px, 0)`); 41 | }); 42 | }); 43 | 44 | describe('on up-left positioning', () => { 45 | it('returns translation from bottom-left of window to bottom-left of reference el', () => { 46 | const styles = getDetachedContainerStyles(OPEN_UP, ANCHOR_LEFT, referenceEl); 47 | const offsetY = -(windowHeight - referenceElRect.bottom); 48 | expect(styles.transform).to.equal(`translate3d(${Math.round(referenceElRect.left)}px, ${Math.round(offsetY)}px, 0)`); 49 | }); 50 | }); 51 | 52 | describe('on down-right positioning', () => { 53 | it('returns translation from top-right of window to top-right of reference el', () => { 54 | const styles = getDetachedContainerStyles(OPEN_DOWN, ANCHOR_RIGHT, referenceEl); 55 | const offsetX = -(windowWidth - referenceElRect.right); 56 | expect(styles.transform).to.equal(`translate3d(${Math.round(offsetX)}px, ${Math.round(referenceElRect.top)}px, 0)`); 57 | }); 58 | }); 59 | 60 | describe('on up-right positioning', () => { 61 | it('returns translation from bottom-right of window to bottom-right of reference el', () => { 62 | const styles = getDetachedContainerStyles(OPEN_UP, ANCHOR_RIGHT, referenceEl); 63 | const offsetX = -(windowWidth - referenceElRect.right); 64 | const offsetY = -(windowHeight - referenceElRect.bottom); 65 | expect(styles.transform).to.equal(`translate3d(${Math.round(offsetX)}px, ${Math.round(offsetY)}px, 0)`); 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/utils/getInputHeight_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import getInputHeight from '../../src/utils/getInputHeight'; 3 | 4 | const theme = { 5 | font: { 6 | input: { 7 | lineHeight: 13, 8 | lineHeight_small: 7, 9 | }, 10 | }, 11 | spacing: { 12 | inputPadding: 10, 13 | displayTextPaddingVertical: 8, 14 | displayTextPaddingTop: 10, 15 | displayTextPaddingBottom: 12, 16 | displayTextPaddingVertical_small: 2, 17 | displayTextPaddingTop_small: 4, 18 | displayTextPaddingBottom_small: 6, 19 | }, 20 | }; 21 | 22 | describe('#getInputHeight', () => { 23 | it('returns the expected value with falsy second arg', () => { 24 | const inputHeight = getInputHeight(theme); 25 | expect(inputHeight).to.equal(55); 26 | }); 27 | 28 | it('returns the expected value with truthy second arg', () => { 29 | const inputHeight = getInputHeight(theme, true); 30 | expect(inputHeight).to.equal(37); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/utils/getNumberOfCalendarMonthWeeks_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import getNumberOfCalendarMonthWeeks from '../../src/utils/getNumberOfCalendarMonthWeeks'; 5 | 6 | describe('getNumberOfCalendarMonthWeeks', () => { 7 | it('returns 4 weeks for a 4-week month', () => { 8 | const february2018 = moment('2018-02-01', 'YYYY-MM-DD'); 9 | expect(getNumberOfCalendarMonthWeeks(february2018, 4)).to.equal(4); 10 | }); 11 | 12 | it('returns 5 weeks for a 5-week month', () => { 13 | const july2018 = moment('2018-07-01', 'YYYY-MM-DD'); 14 | expect(getNumberOfCalendarMonthWeeks(july2018, 0)).to.equal(5); 15 | }); 16 | 17 | it('returns 6 weeks for a 6-week month', () => { 18 | const september2018 = moment('2018-09-01', 'YYYY-MM-DD'); 19 | expect(getNumberOfCalendarMonthWeeks(september2018, 0)).to.equal(6); 20 | }); 21 | 22 | it('changing the first day of week changes the number of weeks', () => { 23 | const september2018 = moment('2018-09-01', 'YYYY-MM-DD'); 24 | expect(getNumberOfCalendarMonthWeeks(september2018, 6)).to.equal(5); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/utils/getPhrasePropTypes_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import getPhrasePropTypes from '../../src/utils/getPhrasePropTypes'; 4 | 5 | const PhraseObject = { 6 | foo: 'x', 7 | bar: 'y', 8 | baz: 'z', 9 | }; 10 | 11 | describe('#getPhrasePropTypes', () => { 12 | it('contains each key from the original object', () => { 13 | const propTypes = getPhrasePropTypes(PhraseObject); 14 | Object.keys(PhraseObject).forEach((key) => { 15 | expect(Object.keys(propTypes).filter((type) => type === key).length).to.not.equal(0); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/utils/getPhrase_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon-sandbox'; 3 | import getPhrase from '../../src/utils/getPhrase'; 4 | 5 | describe('getPhrase', () => { 6 | it('returns empty string when arg is falsy', () => { 7 | expect(getPhrase()).to.equal(''); 8 | }); 9 | 10 | it('returns arg if arg is a string', () => { 11 | const test = 'foobarbaz'; 12 | expect(getPhrase(test)).to.equal(test); 13 | }); 14 | 15 | describe('arg is a function', () => { 16 | it('returns invoked arg', () => { 17 | const test = 'this is a new test string'; 18 | const phraseFunc = sinon.stub().returns(test); 19 | const phrase = getPhrase(phraseFunc); 20 | expect(phraseFunc.callCount).to.equal(1); 21 | expect(phrase).to.equal(test); 22 | }); 23 | 24 | it('passes second arg into the invoked function', () => { 25 | const testObj = { hello: 'world' }; 26 | const phraseFunc = sinon.stub(); 27 | getPhrase(phraseFunc, testObj); 28 | expect(phraseFunc.getCall(0).args[0]).to.equal(testObj); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/utils/getPooledMoment_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import moment from 'moment'; 3 | 4 | import getPooledMoment from '../../src/utils/getPooledMoment'; 5 | 6 | describe('getPooledMoment', () => { 7 | it('returns a moment given a day string', () => { 8 | const momentObj = getPooledMoment('2017-12-10'); 9 | expect(moment.isMoment(momentObj)).to.equal(true); 10 | expect(momentObj.format('YYYY MM DD')).to.equal('2017 12 10'); 11 | }); 12 | 13 | it('returns the same moment given the same day string', () => { 14 | const momentObj1 = getPooledMoment('2017-12-10'); 15 | const momentObj2 = getPooledMoment('2017-12-10'); 16 | expect(momentObj1).to.equal(momentObj2); 17 | }); 18 | 19 | it('returns a different moment given a different day string', () => { 20 | const momentObj1 = getPooledMoment('2017-12-10'); 21 | const momentObj2 = getPooledMoment('2017-12-11'); 22 | expect(momentObj1).not.to.equal(momentObj2); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/utils/getResponsiveContainerStyles_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import wrap from 'mocha-wrap'; 3 | 4 | import getResponsiveContainerStyles from '../../src/utils/getResponsiveContainerStyles'; 5 | 6 | import { ANCHOR_LEFT, ANCHOR_RIGHT } from '../../src/constants'; 7 | 8 | const describeUnlessWindow = typeof window === 'undefined' ? describe : describe.skip; 9 | 10 | describe('#getResponsiveContainerStyles', () => { 11 | describeUnlessWindow('window.innerWidth', () => { 12 | wrap() 13 | .withGlobal('window', () => ({})) 14 | .withOverride(() => window, 'innerWidth', () => -42) 15 | .it('uses window.innerWidth', () => { 16 | const styles = getResponsiveContainerStyles(ANCHOR_LEFT, 0, 0); 17 | expect(styles[ANCHOR_LEFT]).to.equal(window.innerWidth); 18 | }); 19 | }); 20 | 21 | it('returns a numerical value when margin is not included', () => { 22 | const styles = getResponsiveContainerStyles(ANCHOR_LEFT, 0, 0); 23 | expect(styles[ANCHOR_LEFT]).to.be.a('number'); 24 | }); 25 | 26 | it('returns left style for left anchored container', () => { 27 | const styles = getResponsiveContainerStyles(ANCHOR_LEFT, 0, 0, 0); 28 | expect(styles[ANCHOR_LEFT]).to.not.be.an('undefined'); 29 | }); 30 | 31 | it('returns right style for right anchored container', () => { 32 | const styles = getResponsiveContainerStyles(ANCHOR_RIGHT, 0, 0, 0); 33 | expect(styles[ANCHOR_RIGHT]).to.not.be.an('undefined'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/utils/getSelectedDateOffset_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import moment from 'moment'; 3 | 4 | import getSelectedDateOffset from '../../src/utils/getSelectedDateOffset'; 5 | 6 | const today = moment(); 7 | 8 | describe('#getSelectedDateOffset', () => { 9 | it('returns a function modified moment object', () => { 10 | const fn = (day) => day.add(2, 'days'); 11 | const modifiedDay = getSelectedDateOffset(fn, today); 12 | expect(modifiedDay.format()).to.equal(today.clone().add(2, 'days').format()); 13 | }); 14 | 15 | it('returns the passed day when function is undefined', () => { 16 | const modifiedDay = getSelectedDateOffset(undefined, today); 17 | expect(modifiedDay.format()).to.equal(today.format()); 18 | }); 19 | 20 | it('modifies the returned day using the modifier callback', () => { 21 | const fn = (day) => day.add(2, 'days'); 22 | const modifier = (day) => day.subtract(2, 'days'); 23 | const modifiedDay = getSelectedDateOffset(fn, today, modifier); 24 | expect(modifiedDay.format()).to.equal(today.clone().format()); 25 | }); 26 | 27 | it('does not apply the modifier if function is undefined', () => { 28 | const modifier = (day) => day.subtract(2, 'days'); 29 | const modifiedDay = getSelectedDateOffset(undefined, today, modifier); 30 | expect(modifiedDay.format()).to.equal(today.format()); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/utils/getTransformStyles_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import getTransformStyles from '../../src/utils/getTransformStyles'; 4 | 5 | describe('#getTransformStyles', () => { 6 | it('returns non-prefixed transform style', () => { 7 | const TRANSFORM_VALUE = 'foo'; 8 | const transformStyles = getTransformStyles(TRANSFORM_VALUE); 9 | expect(transformStyles.transform).to.equal(TRANSFORM_VALUE); 10 | }); 11 | 12 | it('returns ms-prefixed transform style', () => { 13 | const TRANSFORM_VALUE = 'foo'; 14 | const transformStyles = getTransformStyles(TRANSFORM_VALUE); 15 | expect(transformStyles.msTransform).to.equal(TRANSFORM_VALUE); 16 | }); 17 | 18 | it('returns moz-prefixed transform style', () => { 19 | const TRANSFORM_VALUE = 'foo'; 20 | const transformStyles = getTransformStyles(TRANSFORM_VALUE); 21 | expect(transformStyles.MozTransform).to.equal(TRANSFORM_VALUE); 22 | }); 23 | 24 | it('returns webkit-prefixed transform style', () => { 25 | const TRANSFORM_VALUE = 'foo'; 26 | const transformStyles = getTransformStyles(TRANSFORM_VALUE); 27 | expect(transformStyles.WebkitTransform).to.equal(TRANSFORM_VALUE); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/utils/getVisibleDays_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isSameDay from '../../src/utils/isSameDay'; 5 | import getVisibleDays from '../../src/utils/getVisibleDays'; 6 | 7 | const today = moment(); 8 | 9 | describe('getVisibleDays', () => { 10 | it('has numberOfMonths entries', () => { 11 | const numberOfMonths = 3; 12 | const visibleDays = getVisibleDays(today, numberOfMonths, false); 13 | expect(Object.keys(visibleDays).length).to.equal(numberOfMonths + 2); 14 | }); 15 | 16 | it('values are all arrays of moment objects', () => { 17 | const visibleDays = getVisibleDays(today, 3, false); 18 | Object.values(visibleDays).forEach((days) => { 19 | expect(Array.isArray(days)).to.equal(true); 20 | days.forEach((day) => { 21 | expect(moment.isMoment(day)).to.equal(true); 22 | }); 23 | }); 24 | }); 25 | 26 | it('contains first arg day', () => { 27 | const visibleDays = getVisibleDays(today, 3, false); 28 | const containsToday = Object.values(visibleDays) 29 | .filter((days) => days.filter((day) => isSameDay(day, today)).length > 0); 30 | expect(containsToday.length > 0).to.equal(true); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/utils/isAfterDay_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isAfterDay from '../../src/utils/isAfterDay'; 5 | 6 | const today = moment(); 7 | const tomorrow = moment().add(1, 'days'); 8 | 9 | describe('isAfterDay', () => { 10 | it('returns true if first arg is after the second but have same month and year', () => { 11 | expect(isAfterDay(tomorrow, today)).to.equal(true); 12 | }); 13 | 14 | it('returns true if first arg is after the second but have same day and year', () => { 15 | expect(isAfterDay(moment().clone().add(1, 'month'), today)).to.equal(true); 16 | }); 17 | 18 | it('returns true if first arg is after the second but have same day and month', () => { 19 | expect(isAfterDay(moment().clone().add(1, 'year'), today)).to.equal(true); 20 | }); 21 | 22 | it('returns false if args are the same day', () => { 23 | expect(isAfterDay(today, today)).to.equal(false); 24 | }); 25 | 26 | it('returns false if first arg is after the second', () => { 27 | expect(isAfterDay(today, tomorrow)).to.equal(false); 28 | }); 29 | 30 | describe('non-moment object arguments', () => { 31 | it('is false if first argument is not a moment object', () => { 32 | expect(isAfterDay(null, today)).to.equal(false); 33 | }); 34 | 35 | it('is false if second argument is not a moment object', () => { 36 | expect(isAfterDay(today, 'foo')).to.equal(false); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/utils/isBeforeDay_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isBeforeDay from '../../src/utils/isBeforeDay'; 5 | 6 | const today = moment(); 7 | const tomorrow = moment().add(1, 'days'); 8 | 9 | describe('isBeforeDay', () => { 10 | it('returns true if first arg is before the second but have same month and year', () => { 11 | expect(isBeforeDay(today, tomorrow)).to.equal(true); 12 | }); 13 | 14 | it('returns true if first arg is before the second but have same day and year', () => { 15 | expect(isBeforeDay(today, moment().clone().add(1, 'month'))).to.equal(true); 16 | }); 17 | 18 | it('returns true if first arg is before the second but have same day and month', () => { 19 | expect(isBeforeDay(today, moment().clone().add(1, 'year'))).to.equal(true); 20 | }); 21 | 22 | it('returns false if args are the same day', () => { 23 | expect(isBeforeDay(today, today)).to.equal(false); 24 | }); 25 | 26 | it('returns false if first arg is after the second', () => { 27 | expect(isBeforeDay(tomorrow, today)).to.equal(false); 28 | }); 29 | 30 | describe('non-moment object arguments', () => { 31 | it('is false if first argument is not a moment object', () => { 32 | expect(isBeforeDay(null, today)).to.equal(false); 33 | }); 34 | 35 | it('is false if second argument is not a moment object', () => { 36 | expect(isBeforeDay(today, 'foo')).to.equal(false); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/utils/isDayVisible_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isDayVisible from '../../src/utils/isDayVisible'; 5 | 6 | describe('#isDayVisible', () => { 7 | it('returns true if arg is in visible months', () => { 8 | const test = moment().add(3, 'months'); 9 | const currentMonth = moment().add(2, 'months'); 10 | expect(isDayVisible(test, currentMonth, 2)).to.equal(true); 11 | }); 12 | 13 | it('returns false if arg is before first month', () => { 14 | const test = moment().add(1, 'months'); 15 | const currentMonth = moment().add(2, 'months'); 16 | expect(isDayVisible(test, currentMonth, 2)).to.equal(false); 17 | }); 18 | 19 | it('returns false if arg is after last month', () => { 20 | const test = moment().add(4, 'months'); 21 | const currentMonth = moment().add(2, 'months'); 22 | expect(isDayVisible(test, currentMonth, 2)).to.equal(false); 23 | }); 24 | 25 | describe('enableOutsideDays', () => { 26 | it('returns true if arg is in partial week before visible months', () => { 27 | const test = moment('2019-04-30'); 28 | const currentMonth = moment('2019-05-01'); 29 | expect(isDayVisible(test, currentMonth, 1, false)).to.equal(false); 30 | expect(isDayVisible(test, currentMonth, 1, true)).to.equal(true); 31 | }); 32 | 33 | it('returns true if arg is in partial week after visible months', () => { 34 | const test = moment('2019-06-01'); 35 | const currentMonth = moment('2019-05-01'); 36 | expect(isDayVisible(test, currentMonth, 1, false)).to.equal(false); 37 | expect(isDayVisible(test, currentMonth, 1, true)).to.equal(true); 38 | }); 39 | 40 | it('returns false if arg is before partial week before visible months', () => { 41 | const test = moment('2019-04-27'); 42 | const currentMonth = moment('2019-05-01'); 43 | expect(isDayVisible(test, currentMonth, 1, true)).to.equal(false); 44 | }); 45 | 46 | it('returns false if arg is after partial week after visible months', () => { 47 | const test = moment('2019-06-03'); 48 | const currentMonth = moment('2019-05-01'); 49 | expect(isDayVisible(test, currentMonth, 1, true)).to.equal(false); 50 | }); 51 | }); 52 | 53 | // this test fails when run with the whole suite, 54 | // potentially due to cache pollution from other tests 55 | it.skip('works when the first day of the week that starts the month does not have a midnight', () => { 56 | const march29 = moment('2020-03-29').utcOffset(-1 /* 'Atlantic/Azores' */); 57 | const april2020 = moment('2020-04-02').utcOffset(-1 /* 'Atlantic/Azores' */); 58 | expect(isDayVisible(march29, april2020, 1, true)).to.equal(true); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/utils/isInclusivelyAfterDay_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isInclusivelyAfterDay from '../../src/utils/isInclusivelyAfterDay'; 5 | 6 | const today = moment(); 7 | const tomorrow = moment().add(1, 'days'); 8 | 9 | describe('isInclusivelyAfterDay', () => { 10 | it('returns true if first argument is after the second', () => { 11 | expect(isInclusivelyAfterDay(tomorrow, today)).to.equal(true); 12 | }); 13 | 14 | it('returns true for same day arguments', () => { 15 | expect(isInclusivelyAfterDay(today, today)).to.equal(true); 16 | }); 17 | 18 | it('returns false if first argument is before the second', () => { 19 | expect(isInclusivelyAfterDay(today, tomorrow)).to.equal(false); 20 | }); 21 | 22 | describe('non-moment object arguments', () => { 23 | it('is false if first argument is not a moment object', () => { 24 | expect(isInclusivelyAfterDay(null, today)).to.equal(false); 25 | }); 26 | 27 | it('is false if second argument is not a moment object', () => { 28 | expect(isInclusivelyAfterDay(today, 'foo')).to.equal(false); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/utils/isInclusivelyBeforeDay_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isInclusivelyBeforeDay from '../../src/utils/isInclusivelyBeforeDay'; 5 | 6 | const today = moment(); 7 | const tomorrow = moment().add(1, 'days'); 8 | 9 | describe('isInclusivelyBeforeDay', () => { 10 | it('returns true if first argument is before the second', () => { 11 | expect(isInclusivelyBeforeDay(today, tomorrow)).to.equal(true); 12 | }); 13 | 14 | it('returns true for same day arguments', () => { 15 | expect(isInclusivelyBeforeDay(today, today)).to.equal(true); 16 | }); 17 | 18 | it('returns false if first argument is after the second', () => { 19 | expect(isInclusivelyBeforeDay(tomorrow, today)).to.equal(false); 20 | }); 21 | 22 | describe('non-moment object arguments', () => { 23 | it('is false if first argument is not a moment object', () => { 24 | expect(isInclusivelyBeforeDay(null, today)).to.equal(false); 25 | }); 26 | 27 | it('is false if second argument is not a moment object', () => { 28 | expect(isInclusivelyBeforeDay(today, 'foo')).to.equal(false); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/utils/isNextDay_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isNextDay from '../../src/utils/isNextDay'; 5 | 6 | const today = moment(); 7 | const tomorrow = moment().add(1, 'days'); 8 | 9 | describe('isNextDay', () => { 10 | it('returns true if second argument is the next day after the first', () => { 11 | expect(isNextDay(today, tomorrow)).to.equal(true); 12 | }); 13 | 14 | it('returns false if the second arg is not the next day after the first', () => { 15 | expect(isNextDay(tomorrow, today)).to.equal(false); 16 | }); 17 | 18 | describe('non-moment arguments', () => { 19 | it('is false if first argument is not a moment object', () => { 20 | expect(isNextDay(null, today)).to.equal(false); 21 | }); 22 | 23 | it('is false if second argument is not a moment object', () => { 24 | expect(isNextDay(today, 'foo')).to.equal(false); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/utils/isNextMonth_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isNextMonth from '../../src/utils/isNextMonth'; 5 | 6 | const today = moment(); 7 | const nextMonth = moment().add(1, 'months'); 8 | const twoMonths = moment().add(2, 'months'); 9 | 10 | describe('isNextMonth', () => { 11 | it('returns true if second argument is the next month after the first', () => { 12 | expect(isNextMonth(today, nextMonth)).to.equal(true); 13 | }); 14 | 15 | it('returns false if second argument is not the next month after the first', () => { 16 | expect(isNextMonth(nextMonth, today)).to.equal(false); 17 | }); 18 | 19 | it('returns false if second argument is more than one month after the first', () => { 20 | expect(isNextMonth(today, twoMonths)).to.equal(false); 21 | }); 22 | 23 | describe('non-moment arguments', () => { 24 | it('is false if first argument is not a moment object', () => { 25 | expect(isNextMonth(null, today)).to.equal(false); 26 | }); 27 | 28 | it('is false if second argument is not a moment object', () => { 29 | expect(isNextMonth(today, 'foo')).to.equal(false); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/utils/isPrevMonth_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isPrevMonth from '../../src/utils/isPrevMonth'; 5 | 6 | const today = moment(); 7 | const lastMonth = moment().subtract(1, 'months'); 8 | const twoMonthsAgo = moment().subtract(2, 'months'); 9 | 10 | describe('isPrevMonth', () => { 11 | it('returns true if second argument is the month before the first', () => { 12 | expect(isPrevMonth(today, lastMonth)).to.equal(true); 13 | }); 14 | 15 | it('returns false if second argument is not the month before the first', () => { 16 | expect(isPrevMonth(lastMonth, today)).to.equal(false); 17 | }); 18 | 19 | it('returns false if second argument is more than one month before the first', () => { 20 | expect(isPrevMonth(today, twoMonthsAgo)).to.equal(false); 21 | }); 22 | 23 | describe('non-moment arguments', () => { 24 | it('is false if first argument is not a moment object', () => { 25 | expect(isPrevMonth(null, today)).to.equal(false); 26 | }); 27 | 28 | it('is false if second argument is not a moment object', () => { 29 | expect(isPrevMonth(today, 'foo')).to.equal(false); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/utils/isPreviousDay_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isPreviousDay from '../../src/utils/isPreviousDay'; 5 | 6 | const today = moment(); 7 | const yesterday = moment().subtract(1, 'days'); 8 | 9 | describe('isPreviousDay', () => { 10 | it('returns true if second argument is the day immediately before the first', () => { 11 | expect(isPreviousDay(today, yesterday)).to.equal(true); 12 | }); 13 | 14 | it('returns false if the second arg is not the day immediately before the first', () => { 15 | expect(isPreviousDay(yesterday, today)).to.equal(false); 16 | }); 17 | 18 | describe('non-moment arguments', () => { 19 | it('is false if first argument is not a moment object', () => { 20 | expect(isPreviousDay(null, today)).to.equal(false); 21 | }); 22 | 23 | it('is false if second argument is not a moment object', () => { 24 | expect(isPreviousDay(today, 'foo')).to.equal(false); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/utils/isSameDay_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isSameDay from '../../src/utils/isSameDay'; 5 | 6 | const today = moment(); 7 | const tomorrow = moment().add(1, 'days'); 8 | 9 | describe('isSameDay', () => { 10 | it('returns true if args are the same day', () => { 11 | expect(isSameDay(today, today)).to.equal(true); 12 | }); 13 | 14 | it('returns false if args are not the same day', () => { 15 | expect(isSameDay(today, tomorrow)).to.equal(false); 16 | }); 17 | 18 | it('returns false for same days of week', () => { 19 | // Flags accidentally use of moment's day() function, which returns index 20 | // within the week. 21 | expect(isSameDay( 22 | moment('2000-01-01'), 23 | moment('2000-01-08'), 24 | )).to.equal(false); 25 | }); 26 | 27 | describe('non-moment object arguments', () => { 28 | it('is false if first argument is not a moment object', () => { 29 | expect(isSameDay(null, today)).to.equal(false); 30 | }); 31 | 32 | it('is false if second argument is not a moment object', () => { 33 | expect(isSameDay(today, 'foo')).to.equal(false); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/utils/isSameMonth_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isSameMonth from '../../src/utils/isSameMonth'; 5 | 6 | const today = moment(); 7 | const nextMonth = moment().add(1, 'month'); 8 | 9 | describe('isSameMonth', () => { 10 | it('returns true if args are the same month', () => { 11 | expect(isSameMonth(today, today)).to.equal(true); 12 | }); 13 | 14 | it('returns false if args are not the same month', () => { 15 | expect(isSameMonth(today, nextMonth)).to.equal(false); 16 | }); 17 | 18 | describe('non-moment object arguments', () => { 19 | it('is false if first argument is not a moment object', () => { 20 | expect(isSameMonth(null, today)).to.equal(false); 21 | }); 22 | 23 | it('is false if second argument is not a moment object', () => { 24 | expect(isSameMonth(today, 'foo')).to.equal(false); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/utils/isTransitionEndSupported_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import wrap from 'mocha-wrap'; 3 | 4 | import isTransitionEndSupported from '../../src/utils/isTransitionEndSupported'; 5 | 6 | const describeIfNoWindow = typeof window === 'undefined' ? describe : describe.skip; 7 | 8 | describeIfNoWindow('isTransitionEndSupported', () => { 9 | describe('without `window`', () => { 10 | it('returns false', () => { 11 | expect(typeof window).to.equal('undefined'); 12 | expect(isTransitionEndSupported()).to.equal(false); 13 | }); 14 | }); 15 | 16 | wrap() 17 | .withGlobal('window', () => ({})) 18 | .describe('with `window`', () => { 19 | it('returns false without `window.TransitionEvent`', () => { 20 | expect(isTransitionEndSupported()).to.equal(false); 21 | }); 22 | 23 | wrap() 24 | .withOverride(() => window, 'TransitionEvent', () => () => {}) 25 | .it('returns true with `window.ontouchstart', () => { 26 | expect(isTransitionEndSupported()).to.equal(true); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/utils/noflip_spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import noflip from '../../src/utils/noflip'; 4 | 5 | describe('noflip', () => { 6 | it('appends a noflip comment to a number', () => { 7 | expect(noflip(42)).to.equal('42px /* @noflip */'); 8 | }); 9 | 10 | it('appends a noflip comment to a string', () => { 11 | expect(noflip('foo')).to.equal('foo /* @noflip */'); 12 | }); 13 | 14 | it('throws when value is unexpected type', () => { 15 | expect(() => { 16 | noflip([]); 17 | }).to.throw(TypeError); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/utils/toISODateString_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import toISODateString from '../../src/utils/toISODateString'; 5 | import { ISO_FORMAT } from '../../src/constants'; 6 | 7 | describe('toISODateString', () => { 8 | it('returns null for falsy argument', () => { 9 | expect(toISODateString()).to.equal(null); 10 | }); 11 | 12 | it('converts moment object to localized date string', () => { 13 | const testDate = moment('1991-07-13'); 14 | const dateString = toISODateString(testDate); 15 | expect(dateString).to.equal('1991-07-13'); 16 | }); 17 | 18 | it('matches moment format behavior', () => { 19 | const testDate = moment('1991-07-13'); 20 | const dateString = toISODateString(testDate); 21 | expect(dateString).to.equal(testDate.format(ISO_FORMAT)); 22 | }); 23 | 24 | it('converts iso date string to ISO date string', () => { 25 | const testDate = moment('1991-07-13'); 26 | const dateString = toISODateString(testDate.format(ISO_FORMAT)); 27 | expect(dateString).to.equal('1991-07-13'); 28 | }); 29 | 30 | it('convers localized date strings to ISO date string', () => { 31 | const testDate = moment('1991-07-13'); 32 | const dateString = toISODateString(testDate.format('L')); 33 | expect(dateString).to.equal('1991-07-13'); 34 | }); 35 | 36 | it('converts custom format date strings with format passed in', () => { 37 | const testDate = moment('1991-07-13'); 38 | const dateString = toISODateString(testDate.format('YYYY---DD/MM'), 'YYYY---DD/MM'); 39 | expect(dateString).to.equal('1991-07-13'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/utils/toISOMonthString_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import { ISO_FORMAT, ISO_MONTH_FORMAT } from '../../src/constants'; 5 | import toISOMonthString from '../../src/utils/toISOMonthString'; 6 | 7 | describe('#toISOMonthString', () => { 8 | describe('arg is a moment object', () => { 9 | it('returns month in ISO_MONTH_FORMAT format', () => { 10 | const today = moment(); 11 | expect(toISOMonthString(today)).to.equal(today.format(ISO_MONTH_FORMAT)); 12 | }); 13 | }); 14 | 15 | describe('arg is a string', () => { 16 | describe('arg is in ISO format', () => { 17 | it('returns month in ISO_MONTH_FORMAT format', () => { 18 | const today = moment(); 19 | const todayISO = today.format(ISO_FORMAT); 20 | expect(toISOMonthString(todayISO)).to.equal(today.format(ISO_MONTH_FORMAT)); 21 | }); 22 | }); 23 | 24 | describe('arg matches the second arg date format provided', () => { 25 | it('returns month in ISO_MONTH_FORMAT format', () => { 26 | const today = moment(); 27 | const dateFormat = 'MM_DD_YYYY'; 28 | const formattedDate = today.format(dateFormat); 29 | const monthString = toISOMonthString(formattedDate, dateFormat); 30 | expect(monthString).to.equal(today.format(ISO_MONTH_FORMAT)); 31 | }); 32 | }); 33 | 34 | describe('arg is neither in iso format or in the provided format', () => { 35 | it('returns null', () => { 36 | const today = moment(); 37 | const dateFormat = 'MM_DD_YYYY'; 38 | const formattedDate = today.format('MM-DD-YYYY'); 39 | expect(toISOMonthString(formattedDate, dateFormat)).to.equal(null); 40 | }); 41 | }); 42 | 43 | describe('arg is not a valid date string', () => { 44 | it('returns null', () => { 45 | expect(toISOMonthString('This is not a date')).to.equal(null); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/utils/toLocalizedDateString_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import toLocalizedDateString from '../../src/utils/toLocalizedDateString'; 5 | import { ISO_FORMAT } from '../../src/constants'; 6 | 7 | describe('toLocalizedDateString', () => { 8 | it('returns null for falsy argument', () => { 9 | expect(toLocalizedDateString()).to.equal(null); 10 | }); 11 | 12 | it('converts moment object to localized date string', () => { 13 | const testDate = moment('1991-07-13'); 14 | const dateString = toLocalizedDateString(testDate); 15 | expect(dateString).to.equal(testDate.format('L')); 16 | }); 17 | 18 | it('converts iso date string to localized date string', () => { 19 | const testDate = moment('1991-07-13'); 20 | const dateString = toLocalizedDateString(testDate.format(ISO_FORMAT)); 21 | expect(dateString).to.equal(testDate.format('L')); 22 | }); 23 | 24 | it('localized date strings stay the same', () => { 25 | const testDate = moment('1991-07-13'); 26 | const dateString = toLocalizedDateString(testDate.format('L')); 27 | expect(dateString).to.equal(testDate.format('L')); 28 | }); 29 | 30 | it('converts custom format date strings with format passed in', () => { 31 | const testDate = moment('1991-07-13'); 32 | const dateString = toLocalizedDateString(testDate.format('YYYY---DD/MM'), 'YYYY---DD/MM'); 33 | expect(dateString).to.equal(testDate.format('L')); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/utils/toMomentObject_spec.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { expect } from 'chai'; 3 | 4 | import isSameDay from '../../src/utils/isSameDay'; 5 | import toMomentObject from '../../src/utils/toMomentObject'; 6 | 7 | describe('toMomentObject', () => { 8 | it('returns null for null input', () => { 9 | expect(toMomentObject(null)).to.equal(null); 10 | }); 11 | 12 | it('returns null for undefined input', () => { 13 | expect(toMomentObject(undefined)).to.equal(null); 14 | }); 15 | 16 | it('returns null for empty string', () => { 17 | expect(toMomentObject('')).to.equal(null); 18 | }); 19 | 20 | it('returns null for no input', () => { 21 | expect(toMomentObject()).to.equal(null); 22 | }); 23 | 24 | it('output has time of 12PM', () => { 25 | expect(toMomentObject('1991-07-13').hour()).to.equal(12); 26 | }); 27 | 28 | it('parses custom format', () => { 29 | const date = toMomentObject('1991---13/07', 'YYYY---DD/MM'); 30 | 31 | expect(date).not.to.equal(null); 32 | expect(date.month()).to.equal(6); // moment months are zero-indexed 33 | expect(date.date()).to.equal(13); 34 | expect(date.year()).to.equal(1991); 35 | }); 36 | 37 | it('parses localized format', () => { 38 | const date = toMomentObject(moment('1991-07-13').format('L')); 39 | 40 | expect(date).not.to.equal(null); 41 | expect(date.month()).to.equal(6); // moment months are zero-indexed 42 | expect(date.date()).to.equal(13); 43 | expect(date.year()).to.equal(1991); 44 | }); 45 | 46 | describe('Daylight Savings Time issues', () => { 47 | it('last of February does not equal first of March', () => { 48 | expect(isSameDay(toMomentObject('2017-02-28'), toMomentObject('2017-03-01'))).to.equal(false); 49 | }); 50 | 51 | it('last of March does not equal first of April', () => { 52 | expect(isSameDay(toMomentObject('2017-03-31'), toMomentObject('2017-04-01'))).to.equal(false); 53 | }); 54 | }); 55 | }); 56 | --------------------------------------------------------------------------------