├── .npmignore ├── .npmrc ├── .storybook ├── manager.js ├── MyTheme.js ├── main.js ├── preview.js └── webpack.config.js ├── src ├── types │ ├── SelectDatepickerOrder.ts │ └── SelectDatepickerLabels.ts ├── index.ts ├── components │ ├── OptionsRenderer.tsx │ ├── SelectRenderer.tsx │ └── SelectDatepicker.tsx ├── interfaces │ └── ISelectDatePicker.ts ├── stories │ ├── DemoPicker.tsx │ ├── 1_Welcome.stories.mdx │ └── SelectDatepicker.stories.tsx └── utils │ └── helpers.ts ├── prettier.config.js ├── .gitignore ├── tsconfig.json ├── tsconfig.bundle.json ├── rollup.config.js ├── README.md ├── package.json └── eslint.config.js /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | // .storybook/manager.js 2 | 3 | import { addons } from '@storybook/addons'; 4 | import myTheme from './MyTheme'; 5 | 6 | addons.setConfig({ 7 | theme: myTheme, 8 | }); -------------------------------------------------------------------------------- /src/types/SelectDatepickerOrder.ts: -------------------------------------------------------------------------------- 1 | export type SelectDatepickerOrder = 2 | | 'day/month/year' 3 | | 'day/year/month' 4 | | 'month/day/year' 5 | | 'month/year/day' 6 | | 'year/month/day' 7 | | 'year/day/month'; 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { SelectDatepicker } from './components/SelectDatepicker'; 2 | export { ISelectDatepicker } from './interfaces/ISelectDatePicker'; 3 | export { SelectDatepickerLabels } from './types/SelectDatepickerLabels'; 4 | export { SelectDatepickerOrder } from './types/SelectDatepickerOrder'; 5 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | bracketSpacing: true, 5 | trailingComma: 'es5', 6 | singleQuote: true, 7 | overrides: [ 8 | { 9 | files: '*.html', 10 | options: { parser: 'babel' }, 11 | }, 12 | ], 13 | endOfLine: 'auto', 14 | }; 15 | -------------------------------------------------------------------------------- /.storybook/MyTheme.js: -------------------------------------------------------------------------------- 1 | // .storybook/YourTheme.js 2 | 3 | import { create } from '@storybook/theming'; 4 | 5 | export default create({ 6 | base: 'light', 7 | brandTitle: 'React Select Datepicker', 8 | brandUrl: 'https://jeffmcammond.com', 9 | brandImage: 'https://raw.githubusercontent.com/JMcAmmond/public-assets/main/logo.svg' 10 | }); -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 3 | addons: [ 4 | '@storybook/addon-links', 5 | '@storybook/addon-essentials', 6 | '@storybook/addon-storysource', 7 | '@storybook/addon-a11y', 8 | '@storybook/addon-interactions', 9 | ], 10 | framework: '@storybook/react', 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /node_modules 4 | /.pnp 5 | .pnp.js 6 | 7 | /coverage 8 | 9 | /build 10 | /storybook-static 11 | /dist 12 | 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | expanded: false, 9 | }, 10 | } 11 | 12 | /* 13 | export const decorators = [ 14 | (Story) => ( 15 | <> 16 | {Story()} 17 | 18 | ), 19 | ]; 20 | */ -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config }) => { 2 | config.module.rules.unshift({ 3 | test: /\.svg$/i, 4 | issuer: /\.[jt]sx?$/, 5 | use: [{ 6 | loader: '@svgr/webpack', 7 | options: { 8 | svgoConfig: { 9 | plugins: { 10 | removeViewBox: false 11 | } 12 | } 13 | } 14 | }, 'url-loader'], 15 | }); 16 | return config; 17 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions":{ 3 | "target":"es5", 4 | "module":"esnext", 5 | "moduleResolution": "node", 6 | "strict":true, 7 | "allowSyntheticDefaultImports":true, 8 | "esModuleInterop":true, 9 | "jsx":"react-jsx", 10 | "skipLibCheck":true, 11 | "forceConsistentCasingInFileNames":true, 12 | "declaration": true, 13 | "declarationDir": "dist" 14 | }, 15 | "exclude": ["./dist/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /src/types/SelectDatepickerLabels.ts: -------------------------------------------------------------------------------- 1 | export type Months = { 2 | 1: string; 3 | 2: string; 4 | 3: string; 5 | 4: string; 6 | 5: string; 7 | 6: string; 8 | 7: string; 9 | 8: string; 10 | 9: string; 11 | 10: string; 12 | 11: string; 13 | 12: string; 14 | }; 15 | 16 | export type SelectDatepickerLabels = { 17 | yearLabel?: string; 18 | monthLabel?: string; 19 | dayLabel?: string; 20 | yearPlaceholder?: string; 21 | monthPlaceholder?: string; 22 | dayPlaceholder?: string; 23 | months?: Months; 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/OptionsRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | 3 | interface IOptionsRenderer { 4 | options: { value: number; label: string }[]; 5 | } 6 | 7 | export const OptionsRenderer = ({ options }: IOptionsRenderer) => { 8 | const toRender = useMemo(() => { 9 | return options.map((item, _) => { 10 | return ( 11 | 14 | ); 15 | }); 16 | }, [options]); 17 | 18 | return <>{toRender}; 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions":{ 3 | "target":"es5", 4 | "module":"esnext", 5 | "moduleResolution": "node", 6 | "strict":true, 7 | "allowSyntheticDefaultImports":true, 8 | "esModuleInterop":true, 9 | "jsx":"react", 10 | "skipLibCheck":true, 11 | "forceConsistentCasingInFileNames":true, 12 | "declaration": true, 13 | "declarationDir": "./dist" 14 | }, 15 | "exclude": [ 16 | "./src/stories", 17 | "./dist/**/*", 18 | "./utils/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/ISelectDatePicker.ts: -------------------------------------------------------------------------------- 1 | import { SelectDatepickerLabels } from '../types/SelectDatepickerLabels'; 2 | import { SelectDatepickerOrder } from '../types/SelectDatepickerOrder'; 3 | 4 | export interface ISelectDatepicker { 5 | id?: string; 6 | className?: string; 7 | minDate?: Date; 8 | maxDate?: Date; 9 | selectedDate?: Date | null; 10 | onDateChange: (date: Date | null) => void; 11 | labels?: SelectDatepickerLabels; 12 | disabled?: boolean; 13 | hasError?: boolean; 14 | monthRef?: React.LegacyRef; 15 | yearRef?: React.LegacyRef; 16 | dayRef?: React.LegacyRef; 17 | reverseYears?: boolean; 18 | hideLabels?: boolean; 19 | order?: SelectDatepickerOrder; 20 | } 21 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable import/no-default-export */ 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import typescript from 'rollup-plugin-typescript2'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import external from 'rollup-plugin-peer-deps-external'; 7 | import postcss from 'rollup-plugin-postcss'; 8 | import url from '@rollup/plugin-url'; 9 | import svgr from '@svgr/rollup'; 10 | 11 | export default { 12 | input: ['./src/index.ts'], 13 | output: [ 14 | { 15 | dir: 'dist', 16 | format: 'es', 17 | preserveModules: true, 18 | preserveModulesRoot: 'src', 19 | sourcemap: false, 20 | }, 21 | ], 22 | plugins: [ 23 | external(), 24 | commonjs(), 25 | typescript({ tsconfig: './tsconfig.bundle.json' }), 26 | url({ 27 | include: ['**/*.ttf', '**/*.svg'], 28 | limit: Infinity, 29 | }), 30 | svgr({ icon: true }), 31 | postcss(), 32 | terser(), 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /src/stories/DemoPicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { SelectDatepicker } from '../components/SelectDatepicker'; 4 | 5 | const StyledDatePicker = styled(SelectDatepicker)` 6 | max-width: 500px; 7 | width: 100%; 8 | 9 | & > div { 10 | margin: 0 5px; 11 | width: 100%; 12 | } 13 | 14 | select { 15 | color: rgb(68, 68, 68); 16 | font-size: 13px; 17 | padding: 10px 5px; 18 | border-radius: 3px; 19 | cursor: pointer; 20 | 21 | &:disabled { 22 | cursor: not-allowed; 23 | } 24 | } 25 | 26 | label { 27 | font-size: 0.8rem; 28 | margin-bottom: 5px; 29 | font-weight: 600; 30 | } 31 | 32 | option { 33 | font-size: 16px; 34 | } 35 | `; 36 | 37 | const DemoPicker = () => { 38 | const [value, setValue] = useState(null); 39 | 40 | useEffect(() => { 41 | console.log(value); 42 | }, [value]); 43 | 44 | return setValue(date)} selectedDate={value} />; 45 | }; 46 | 47 | export { DemoPicker }; 48 | -------------------------------------------------------------------------------- /src/components/SelectRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classPrefix } from '../utils/helpers'; 3 | 4 | interface ISelectRenderer { 5 | id: string; 6 | labels: { 7 | hide?: boolean; 8 | main?: string; 9 | placeholder?: string; 10 | }; 11 | value: number; 12 | disabled?: boolean; 13 | onChangeHandler: (e: React.ChangeEvent) => void; 14 | selectOptions: JSX.Element; 15 | ref?: React.LegacyRef; 16 | } 17 | 18 | export const SelectRenderer = ({ 19 | id, 20 | labels, 21 | value, 22 | disabled, 23 | onChangeHandler, 24 | selectOptions, 25 | ref, 26 | }: ISelectRenderer) => { 27 | return ( 28 |
32 | {!labels.hide && } 33 | 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-select-datepicker 2 | 3 | A simple and reusable dropdown datepicker component for React ([Demo](https://jeffmcammond.com/react-select-datepicker/)) 4 | 5 | [![NPM](https://img.shields.io/npm/v/react-select-datepicker.svg)](https://www.npmjs.com/package/react-select-datepicker) 6 | ![npm bundle size](https://img.shields.io/bundlephobia/min/react-select-datepicker) 7 | ![GitHub contributors](https://img.shields.io/github/contributors/jmcammond/react-select-datepicker) 8 | ![npm](https://img.shields.io/npm/dt/react-select-datepicker) 9 | ![NPM](https://img.shields.io/npm/l/react-select-datepicker) 10 | 11 | ![Select Datepicker](https://raw.githubusercontent.com/JMcAmmond/public-assets/main/react-select-datepicker.PNG 'Select Datepicker') 12 | 13 | ## Install 14 | 15 | ```bash 16 | npm install --save react-select-datepicker 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```tsx 22 | import React, { useState, useCallback } from 'react'; 23 | import { SelectDatepicker } from 'react-select-datepicker'; 24 | 25 | export const App = () => { 26 | const [value, setValue] = useState(); 27 | 28 | const onDateChange = useCallback((date: Date) => { 29 | setValue(date); 30 | }, []); 31 | 32 | return ( 33 | 37 | ); 38 | }; 39 | ``` 40 | 41 | ## License 42 | 43 | MIT © [JMcAmmond](https://github.com/JMcAmmond) 44 | -------------------------------------------------------------------------------- /src/stories/1_Welcome.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | import { DemoPicker } from './DemoPicker.tsx'; 3 | 4 | 5 | 6 | 30 | 31 |
32 | 33 | ![Select Datepicker](https://raw.githubusercontent.com/JMcAmmond/public-assets/main/logo.svg 'Select Datepicker') 34 | 35 |

36 | A simple and reusable dropdown datepicker component for React. 37 |

38 | 39 | [![NPM](https://img.shields.io/npm/v/react-select-datepicker.svg)](https://www.npmjs.com/package/react-select-datepicker) 40 | ![npm bundle size](https://img.shields.io/bundlephobia/min/react-select-datepicker) 41 | ![GitHub contributors](https://img.shields.io/github/contributors/jmcammond/react-select-datepicker) 42 | ![npm](https://img.shields.io/npm/dt/react-select-datepicker) 43 | ![NPM](https://img.shields.io/npm/l/react-select-datepicker) 44 | 45 |
46 | 47 |
48 | 49 |
50 | 51 | 52 | ### Install 53 | 54 | ```bash 55 | npm install --save react-select-datepicker 56 | ``` 57 | 58 | ### Usage 59 | ```tsx 60 | import React, { useState, useCallback } from 'react'; 61 | import { SelectDatepicker } from 'react-select-datepicker'; 62 | 63 | export const App = () => { 64 | const [value, setValue] = useState(); 65 | 66 | const onDateChange = useCallback((date: Date) => { 67 | setValue(date); 68 | }, []); 69 | 70 | return ( 71 | 75 | ); 76 | }; 77 | ``` -------------------------------------------------------------------------------- /src/stories/SelectDatepicker.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { SelectDatepicker } from '../components/SelectDatepicker'; 4 | 5 | export default { 6 | title: 'SelectDatepicker', 7 | component: SelectDatepicker, 8 | argTypes: { minDate: { control: 'date' } }, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => { 12 | args.minDate = args.minDate ? new Date(args.minDate) : undefined; 13 | args.maxDate = args.maxDate ? new Date(args.maxDate) : undefined; 14 | const [selected, setSelected] = useState( 15 | args.selectedDate ? new Date(args.selectedDate) : undefined 16 | ); 17 | 18 | const handleDateChange = useCallback((date) => { 19 | setSelected(date); 20 | }, []); 21 | 22 | useEffect( 23 | () => setSelected(args.selectedDate ? new Date(args.selectedDate) : undefined), 24 | [args.selectedDate] 25 | ); 26 | 27 | return ; 28 | }; 29 | 30 | export const Default = Template.bind({}); 31 | 32 | export const DateRange = Template.bind({}); 33 | DateRange.args = { 34 | minDate: new Date('10/10/2013'), 35 | maxDate: new Date('01/23/2019'), 36 | }; 37 | 38 | export const SetDate = Template.bind({}); 39 | SetDate.args = { 40 | selectedDate: new Date('02/16/2019'), 41 | }; 42 | 43 | export const Labels = Template.bind({}); 44 | Labels.args = { 45 | labels: { 46 | yearLabel: 'Año', 47 | monthLabel: 'Mes', 48 | dayLabel: 'Día', 49 | yearPlaceholder: 'Año', 50 | monthPlaceholder: 'Mes', 51 | dayPlaceholder: 'Día', 52 | months: { 53 | 1: 'enero', 54 | 2: 'febrero', 55 | 3: 'marzo', 56 | 4: 'abril', 57 | 5: 'mayo', 58 | 6: 'junio', 59 | 7: 'julio', 60 | 8: 'agosto', 61 | 9: 'septiembre', 62 | 10: 'octubre', 63 | 11: 'noviembre', 64 | 12: 'diciembre', 65 | }, 66 | }, 67 | }; 68 | 69 | export const Order = Template.bind({}); 70 | Order.args = { 71 | order: 'day/year/month', 72 | }; 73 | 74 | export const ReverseYears = Template.bind({}); 75 | ReverseYears.args = { 76 | reverseYears: true, 77 | }; 78 | 79 | export const Disabled = Template.bind({}); 80 | Disabled.args = { 81 | disabled: true, 82 | }; 83 | 84 | export const HideLabels = Template.bind({}); 85 | HideLabels.args = { 86 | hideLabels: true, 87 | }; 88 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Months } from '../types/SelectDatepickerLabels'; 2 | 3 | export const classPrefix = 'rsd_'; 4 | 5 | export const range = (start: number, stop: number, step: number) => 6 | Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step); 7 | 8 | export const getAllDaysInMonth = (year: number, month: number) => { 9 | const date = new Date(`${year}/${month}/01`); 10 | const dates = []; 11 | 12 | while (date.getMonth() + 1 === month) { 13 | dates.push(new Date(date).getDate()); 14 | date.setDate(date.getDate() + 1); 15 | } 16 | 17 | return dates; 18 | }; 19 | 20 | export const englishMonths: Months = { 21 | 1: 'January', 22 | 2: 'February', 23 | 3: 'March', 24 | 4: 'April', 25 | 5: 'May', 26 | 6: 'June', 27 | 7: 'July', 28 | 8: 'August', 29 | 9: 'September', 30 | 10: 'October', 31 | 11: 'November', 32 | 12: 'December', 33 | }; 34 | 35 | export const getYearsObject = (start?: Date, end?: Date, reverse?: boolean) => { 36 | let arr = range( 37 | end ? end.getFullYear() : new Date().getFullYear(), 38 | start ? start.getFullYear() : 1900, 39 | -1 40 | ); 41 | if (reverse) { 42 | arr = arr.reverse(); 43 | } 44 | 45 | return arr.map((item, _) => { 46 | return { value: item, label: `${item}` }; 47 | }); 48 | }; 49 | 50 | export const getMonthsObject = ( 51 | start?: Date, 52 | end?: Date, 53 | selectedYear = -1, 54 | months: { [key: string]: string } = englishMonths 55 | ) => { 56 | let arr = range(1, 12, 1); 57 | if (end && selectedYear !== -1 && selectedYear === end.getFullYear()) { 58 | const endMonth = end.getMonth() + 1; 59 | arr = arr.slice(0, endMonth); 60 | } 61 | 62 | if (start && selectedYear !== -1 && selectedYear === start.getFullYear()) { 63 | const startMonth = start.getMonth() + 1; 64 | arr = arr.slice(startMonth - 1, arr.length); 65 | } 66 | 67 | return arr.map((item, _) => { 68 | return { value: item, label: months[item] }; 69 | }); 70 | }; 71 | 72 | export const getDaysObject = (start?: Date, end?: Date, selectedMonth = -1, selectedYear = -1) => { 73 | let arr: number[] = []; 74 | 75 | //Return 31 days if no month was selected 76 | if (selectedMonth === -1) { 77 | arr = range(1, 31, 1); 78 | return arr.map((item, _) => { 79 | return { value: item, label: `${item}` }; 80 | }); 81 | } 82 | 83 | //If no year was selected the pretend its 1900 84 | if (selectedYear === -1) { 85 | arr = getAllDaysInMonth(1900, selectedMonth); 86 | } else { 87 | arr = getAllDaysInMonth(selectedYear, selectedMonth); 88 | } 89 | 90 | //Splice days if have an end date and you are in the correct month and year 91 | if (end && selectedYear === end.getFullYear() && selectedMonth === end.getMonth() + 1) { 92 | const endDay = end.getDate(); 93 | arr = arr.slice(0, endDay); 94 | } 95 | 96 | //Splice days if have an start date and you are in the correct month and year 97 | if (start && selectedYear === start.getFullYear() && selectedMonth === start.getMonth() + 1) { 98 | const startDay = start.getDate(); 99 | arr = arr.slice(startDay - 1, arr.length); 100 | } 101 | 102 | return arr.map((item, _) => { 103 | return { value: item, label: `${item}` }; 104 | }); 105 | }; 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-select-datepicker", 3 | "version": "2.1.2", 4 | "description": "A simple and reusable dropdown datepicker component for React", 5 | "author": "JMcAmmond", 6 | "license": "MIT", 7 | "repository": "JMcAmmond/react-select-datepicker", 8 | "bugs": { 9 | "url": "https://github.com/JMcAmmond/react-select-datepicker/issues" 10 | }, 11 | "homepage": "https://github.com/JMcAmmond/react-select-datepicker#readme", 12 | "main": "dist/index.js", 13 | "source": "src/index.tsx", 14 | "keywords": [ 15 | "react", 16 | "datepicker", 17 | "date", 18 | "select", 19 | "reactjs", 20 | "dropdown", 21 | "datepicker-component" 22 | ], 23 | "files": [ 24 | "dist" 25 | ], 26 | "dependencies": {}, 27 | "peerDependencies": { 28 | "react": ">=17.0.2" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.17.0", 32 | "@rollup/plugin-commonjs": "^21.0.2", 33 | "@rollup/plugin-node-resolve": "^13.1.3", 34 | "@rollup/plugin-url": "^6.1.0", 35 | "@storybook/addon-a11y": "^6.5.12", 36 | "@storybook/addon-actions": "^6.5.12", 37 | "@storybook/addon-essentials": "^6.5.12", 38 | "@storybook/addon-interactions": "^6.5.12", 39 | "@storybook/addon-links": "^6.5.12", 40 | "@storybook/addon-storysource": "^6.5.12", 41 | "@storybook/react": "^6.5.12", 42 | "@storybook/testing-library": "0.0.13", 43 | "@svgr/rollup": "^6.2.1", 44 | "@svgr/webpack": "^6.3.1", 45 | "@testing-library/jest-dom": "^5.16.2", 46 | "@testing-library/react": "^12.1.2", 47 | "@testing-library/user-event": "^13.5.0", 48 | "@types/jest": "^27.4.0", 49 | "@types/node": "^16.11.22", 50 | "@types/react": "^17.0.39", 51 | "@types/react-dom": "^17.0.11", 52 | "@types/styled-components": "^5.1.25", 53 | "@typescript-eslint/eslint-plugin": "^5.12.1", 54 | "@typescript-eslint/parser": "^5.12.1", 55 | "babel-loader": "^8.2.3", 56 | "eslint": "^8.8.0", 57 | "eslint-config-airbnb": "^19.0.4", 58 | "eslint-config-airbnb-typescript": "^16.1.0", 59 | "eslint-config-prettier": "^8.4.0", 60 | "eslint-plugin-eslint-comments": "^3.2.0", 61 | "eslint-plugin-html": "^6.2.0", 62 | "eslint-plugin-import": "^2.25.4", 63 | "eslint-plugin-jest": "^26.1.1", 64 | "eslint-plugin-json": "^3.1.0", 65 | "eslint-plugin-json-files": "^1.3.0", 66 | "eslint-plugin-jsx-a11y": "^6.5.1", 67 | "eslint-plugin-prettier": "^4.0.0", 68 | "eslint-plugin-progress": "0.0.1", 69 | "eslint-plugin-react": "^7.28.0", 70 | "eslint-plugin-react-hooks": "^4.3.0", 71 | "eslint-plugin-storybook": "^0.6.4", 72 | "eslint-plugin-unused-imports": "^2.0.0", 73 | "gh-pages": "^3.2.3", 74 | "jest": "^27.5.1", 75 | "postcss": "^8.4.7", 76 | "prettier": "^2.5.1", 77 | "react": "^17.0.2", 78 | "react-dom": "^17.0.2", 79 | "rollup": "^2.68.0", 80 | "rollup-plugin-generate-package-json": "^3.2.0", 81 | "rollup-plugin-peer-deps-external": "^2.2.4", 82 | "rollup-plugin-postcss": "^4.0.2", 83 | "rollup-plugin-terser": "^7.0.2", 84 | "rollup-plugin-typescript2": "^0.31.2", 85 | "styled-components": "^5.3.5", 86 | "tslib": "^2.3.1", 87 | "typescript": "^4.6.2", 88 | "url-loader": "^4.1.1" 89 | }, 90 | "scripts": { 91 | "test": "npm run test:eslint && npm run test:conflicts && npm run test:ts", 92 | "test:eslint": "npx eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives ./src", 93 | "test:conflicts": "eslint-config-prettier .eslint.config.js", 94 | "test:ts": "npx tsc -p ./tsconfig.json --noEmit", 95 | "start": "start-storybook -p 6006", 96 | "bundle": "rollup -c", 97 | "clean": "rm -rf ./dist ./storybook-static", 98 | "build": "npm run clean && build-storybook && npm run bundle", 99 | "deploy": "gh-pages -d storybook-static" 100 | }, 101 | "eslintConfig": { 102 | "extends": [ 103 | "./eslint.config.js" 104 | ], 105 | "parserOptions": { 106 | "project": [ 107 | "./tsconfig.json" 108 | ] 109 | } 110 | }, 111 | "browserslist": { 112 | "production": [ 113 | ">0.2%", 114 | "not dead", 115 | "not op_mini all" 116 | ], 117 | "development": [ 118 | "last 1 chrome version", 119 | "last 1 firefox version", 120 | "last 1 safari version" 121 | ] 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/components/SelectDatepicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from 'react'; 2 | import { ISelectDatepicker } from '../interfaces/ISelectDatePicker'; 3 | import { classPrefix, getDaysObject, getMonthsObject, getYearsObject } from '../utils/helpers'; 4 | import { OptionsRenderer } from './OptionsRenderer'; 5 | import { SelectRenderer } from './SelectRenderer'; 6 | 7 | const SelectDatepicker = ({ 8 | id, 9 | className, 10 | minDate, 11 | maxDate, 12 | selectedDate, 13 | onDateChange, 14 | disabled = false, 15 | hasError = false, 16 | monthRef, 17 | yearRef, 18 | dayRef, 19 | labels = {}, 20 | order = 'month/day/year', 21 | reverseYears, 22 | hideLabels, 23 | ...args 24 | }: ISelectDatepicker) => { 25 | const [year, setYear] = useState(-1); 26 | const [month, setMonth] = useState(-1); 27 | const [day, setDay] = useState(-1); 28 | 29 | const orderArray = useMemo(() => (order ? order.split('/') : ['month', 'day', 'year']), [order]); 30 | const combinedClassNames = useMemo( 31 | () => [`${classPrefix}_react-select-datepicker`, className].join(' '), 32 | [className] 33 | ); 34 | 35 | const yearOptions = useMemo( 36 | () => , 37 | [maxDate, reverseYears, minDate] 38 | ); 39 | const monthOptions = useMemo( 40 | () => , 41 | [maxDate, labels.months, minDate, year] 42 | ); 43 | const dayOptions = useMemo( 44 | () => , 45 | [maxDate, month, minDate, year] 46 | ); 47 | 48 | const handleYearChange = useCallback( 49 | (e: React.ChangeEvent) => { 50 | setYear(Number(e.target.value)); 51 | 52 | //Validate if current month is in new month options 53 | const mOptions = getMonthsObject(minDate, maxDate, Number(e.target.value)); 54 | if (!mOptions.some((val) => val.value === month)) { 55 | setMonth(-1); 56 | } 57 | 58 | //Validate if current day is in new day options 59 | const dOptions = getDaysObject(minDate, maxDate, month, Number(e.target.value)); 60 | if (!dOptions.some((val) => val.value === day)) { 61 | setDay(-1); 62 | } 63 | }, 64 | [day, month, minDate, maxDate] 65 | ); 66 | 67 | const handleMonthChange = useCallback( 68 | (e: React.ChangeEvent) => { 69 | setMonth(Number(e.target.value)); 70 | 71 | //Validate if current day is in new day options 72 | const dOptions = getDaysObject(minDate, maxDate, Number(e.target.value), year); 73 | if (!dOptions.some((val) => val.value === day)) { 74 | setDay(-1); 75 | } 76 | }, 77 | [day, year, minDate, maxDate] 78 | ); 79 | 80 | const handleDayChange = useCallback((e: React.ChangeEvent) => { 81 | setDay(Number(e.target.value)); 82 | }, []); 83 | 84 | const field: { day: JSX.Element; month: JSX.Element; year: JSX.Element } = useMemo(() => { 85 | return { 86 | day: ( 87 | 100 | ), 101 | month: ( 102 | 115 | ), 116 | year: ( 117 | 130 | ), 131 | }; 132 | }, [ 133 | day, 134 | dayOptions, 135 | dayRef, 136 | disabled, 137 | handleDayChange, 138 | handleMonthChange, 139 | handleYearChange, 140 | hideLabels, 141 | labels.dayLabel, 142 | labels.dayPlaceholder, 143 | labels.monthLabel, 144 | labels.monthPlaceholder, 145 | labels.yearLabel, 146 | labels.yearPlaceholder, 147 | month, 148 | monthOptions, 149 | monthRef, 150 | year, 151 | yearOptions, 152 | yearRef, 153 | ]); 154 | 155 | useEffect(() => { 156 | if (selectedDate !== undefined && selectedDate !== null) { 157 | setDay(Number(selectedDate.getDate())); 158 | setMonth(Number(selectedDate.getMonth() + 1)); 159 | setYear(Number(selectedDate.getFullYear())); 160 | } 161 | }, [selectedDate]); 162 | 163 | useEffect(() => { 164 | if (year !== -1 && month !== -1 && day !== -1) { 165 | onDateChange(new Date(`${month}/${day}/${year}`)); 166 | } else { 167 | onDateChange(null); 168 | } 169 | // eslint-disable-next-line react-hooks/exhaustive-deps 170 | }, [day, month, year]); 171 | 172 | return ( 173 |
180 | {orderArray.map((key, i) => { 181 | return ( 182 | 183 | {field[key as 'day' | 'month' | 'year']} 184 | 185 | ); 186 | })} 187 |
188 | ); 189 | }; 190 | 191 | export { SelectDatepicker }; 192 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb-typescript', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:eslint-comments/recommended', 6 | 'plugin:jest/recommended', 7 | 'plugin:import/react-native', 8 | 'plugin:react/recommended', 9 | 'plugin:react-hooks/recommended', 10 | 'plugin:storybook/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | plugins: [ 14 | 'progress', 15 | '@typescript-eslint', 16 | 'import', 17 | 'json', 18 | 'html', 19 | 'jsx-a11y', 20 | 'prettier', 21 | 'unused-imports', 22 | ], 23 | env: { 24 | es6: true, 25 | 'jest/globals': true, 26 | node: true, 27 | }, 28 | root: true, 29 | parser: '@typescript-eslint/parser', 30 | parserOptions: { 31 | ecmaFeatures: { 32 | jsx: true, 33 | }, 34 | ecmaVersion: 8, 35 | sourceType: 'module', 36 | extraFileExtensions: ['.html', '.md', '.json', '.svg', '.tag'], 37 | project: ['./tsconfig.json'], 38 | }, 39 | settings: { 40 | react: { 41 | version: 'detect', 42 | }, 43 | 'html/html-extensions': ['.html'], 44 | 'import/core-modules': ['enzyme'], 45 | 'import/ignore': ['node_modules'], 46 | 'import/resolver': { 47 | node: { 48 | extensions: ['.js', '.ts', '.tsx', '.mjs', '.d.ts'], 49 | paths: ['node_modules/', 'node_modules/@types/'], 50 | }, 51 | }, 52 | }, 53 | overrides: [ 54 | { 55 | files: ['**/*.tsx', '**/*.ts'], 56 | rules: { 57 | 'react/require-default-props': 'off', 58 | 'react/prop-types': 'off', // we should use types 59 | 'react/forbid-prop-types': 'off', // we should use types 60 | }, 61 | }, 62 | { 63 | files: ['**/*.d.ts'], 64 | rules: { 65 | 'vars-on-top': 'off', 66 | 'no-var': 'off', // this is how typescript works 67 | 'spaced-comment': 'off', 68 | }, 69 | }, 70 | { 71 | files: ['**/*.stories.tsx'], 72 | rules: { 73 | 'import/no-default-export': 'off', 74 | }, 75 | }, 76 | ], 77 | 78 | rules: { 79 | '@typescript-eslint/ban-ts-ignore': 'off', 80 | '@typescript-eslint/camelcase': 'off', 81 | '@typescript-eslint/explicit-function-return-type': 'off', 82 | '@typescript-eslint/explicit-member-accessibility': 'off', 83 | '@typescript-eslint/explicit-module-boundary-types': 'off', 84 | '@typescript-eslint/interface-name-prefix': 'off', 85 | '@typescript-eslint/no-empty-function': 'off', 86 | '@typescript-eslint/no-explicit-any': 'off', 87 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 88 | '@typescript-eslint/no-unused-vars': 'off', 89 | '@typescript-eslint/no-use-before-define': 'off', 90 | '@typescript-eslint/no-var-requires': 'off', 91 | 'progress/activate': 1, 92 | 'class-methods-use-this': 'off', 93 | 'import/default': 'error', 94 | 'import/extensions': [ 95 | 'error', 96 | 'never', 97 | { 98 | ignorePackages: true, 99 | json: 'always', 100 | md: 'always', 101 | svg: 'always', 102 | tag: 'always', 103 | }, 104 | ], 105 | 'import/named': 'error', 106 | 'import/namespace': 'error', 107 | 'import/no-extraneous-dependencies': [ 108 | 'error', 109 | { 110 | devDependencies: [ 111 | 'examples/**', 112 | 'examples-native/**', 113 | '**/example/**', 114 | '*.js', 115 | '**/*.test.js', 116 | '**/*.stories.*', 117 | '**/scripts/*.js', 118 | '**/stories/**/*.js', 119 | '**/stories/**/*.*', 120 | '**/__tests__/**/*.js', 121 | '**/.storybook/**/*.*', 122 | ], 123 | peerDependencies: true, 124 | }, 125 | ], 126 | 'import/no-unresolved': [ 127 | 'error', 128 | { 129 | ignore: ['@storybook'], 130 | }, 131 | ], 132 | 'import/prefer-default-export': 'off', 133 | 'json/*': ['error', 'allowComments'], 134 | 'jsx-a11y/accessible-emoji': 'off', 135 | 'jsx-a11y/anchor-is-valid': [ 136 | 'error', 137 | { 138 | components: ['A', 'LinkTo', 'Link'], 139 | specialLink: ['overrideParams', 'kind', 'story', 'to'], 140 | }, 141 | ], 142 | 'jsx-a11y/label-has-associated-control': [ 143 | 'warn', 144 | { 145 | controlComponents: ['CustomInput'], 146 | depth: 3, 147 | labelAttributes: ['label'], 148 | labelComponents: ['CustomInputLabel'], 149 | }, 150 | ], 151 | 'jsx-a11y/label-has-for': [ 152 | 'error', 153 | { 154 | required: { 155 | some: ['nesting', 'id'], 156 | }, 157 | }, 158 | ], 159 | 'max-classes-per-file': 'off', 160 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 161 | 'no-restricted-imports': [ 162 | 'error', 163 | { 164 | paths: [ 165 | { 166 | name: 'lodash.isequal', 167 | message: 168 | 'Lodash modularized (and lodash < 4.17.11) have CVE vulnerabilities. Please use tree-shakeable imports like lodash/xxx instead', 169 | }, 170 | { 171 | name: 'lodash.uniqueId', 172 | message: 173 | 'Lodash modularized (and lodash < 4.17.11) have CVE vulnerabilities. Please use tree-shakeable imports like lodash/xxx instead', 174 | }, 175 | { 176 | name: 'lodash.mergewith', 177 | message: 178 | 'Lodash modularized (and lodash < 4.17.11) have CVE vulnerabilities. Please use tree-shakeable imports like lodash/xxx instead', 179 | }, 180 | { 181 | name: 'lodash.pick', 182 | message: 183 | 'Lodash modularized (and lodash < 4.17.11) have CVE vulnerabilities. Please use tree-shakeable imports like lodash/xxx instead', 184 | }, 185 | ], 186 | // catch-all for any lodash modularized. 187 | // The CVE is listed against the entire family for lodash < 4.17.11 188 | patterns: ['lodash.*'], 189 | }, 190 | ], 191 | 'no-underscore-dangle': [ 192 | 'error', 193 | { 194 | allow: [ 195 | '__STORYBOOK_CLIENT_API__', 196 | '__STORYBOOK_ADDONS_CHANNEL__', 197 | '__STORYBOOK_STORY_STORE__', 198 | ], 199 | }, 200 | ], 201 | 'react/jsx-filename-extension': [ 202 | 'warn', 203 | { 204 | extensions: ['.js', '.jsx', '.tsx'], 205 | }, 206 | ], 207 | 'react/jsx-fragments': 'off', 208 | 'react/jsx-no-bind': [ 209 | 'error', 210 | { 211 | allowArrowFunctions: true, 212 | allowBind: true, 213 | allowFunctions: true, 214 | ignoreDOMComponents: true, 215 | ignoreRefs: true, 216 | }, 217 | ], 218 | 'react/jsx-props-no-spreading': 'off', 219 | 'react/no-unescaped-entities': 'off', 220 | 'react/sort-comp': [ 221 | 'error', 222 | { 223 | groups: { 224 | staticLifecycle: ['displayName', 'propTypes', 'defaultProps', 'getDerivedStateFromProps'], 225 | }, 226 | order: [ 227 | 'staticLifecycle', 228 | 'static-methods', 229 | 'instance-variables', 230 | 'lifecycle', 231 | '/^on.+$/', 232 | '/^(get|set)(?!(DerivedStateFromProps|SnapshotBeforeUpdate$)).+$/', 233 | 'instance-methods', 234 | 'instance-variables', 235 | 'everything-else', 236 | 'render', 237 | ], 238 | }, 239 | ], 240 | 'react/state-in-constructor': 'off', 241 | 'react/static-property-placement': 'off', 242 | 'prettier/prettier': [ 243 | 'error', 244 | { 245 | endOfLine: 'auto', 246 | }, 247 | ], 248 | '@typescript-eslint/no-unused-vars': 'off', 249 | 'unused-imports/no-unused-imports': 'error', 250 | 'unused-imports/no-unused-vars': [ 251 | 'warn', 252 | { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }, 253 | ], 254 | 'import/no-default-export': 'error', 255 | 'react/react-in-jsx-scope': 'error', 256 | }, 257 | }; 258 | --------------------------------------------------------------------------------