From 38c5cb8d48c9ad2fab815316294c4dae37150d89 Mon Sep 17 00:00:00 2001 From: AlexisG Date: Tue, 24 Dec 2024 13:45:21 +0100 Subject: [PATCH] feat(DatePicker): Add DatePicker component In progress --- docs/styleguide.config.js | 1 + package.json | 2 + react/DatePicker/Readme.md | 16 ++ react/DatePicker/index.jsx | 253 ++++++++++++++++++++ react/DatePicker/locales/en.json | 8 + react/DatePicker/locales/fr.json | 8 + react/DatePicker/locales/withOwnLocales.jsx | 10 + react/index.js | 1 + yarn.lock | 55 ++++- 9 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 react/DatePicker/Readme.md create mode 100644 react/DatePicker/index.jsx create mode 100644 react/DatePicker/locales/en.json create mode 100644 react/DatePicker/locales/fr.json create mode 100644 react/DatePicker/locales/withOwnLocales.jsx diff --git a/docs/styleguide.config.js b/docs/styleguide.config.js index ee4c491b6c..600f9c0a33 100644 --- a/docs/styleguide.config.js +++ b/docs/styleguide.config.js @@ -81,6 +81,7 @@ module.exports = { '../react/ContactPicker', '../react/CozyDialogs', '../react/CozyDialogs/SpecificDialogs', + '../react/DatePicker', '../react/FileImageLoader', '../react/FilePicker', '../react/HistoryRow', diff --git a/package.json b/package.json index d97ad4ee3b..d3a188f8c6 100644 --- a/package.json +++ b/package.json @@ -156,8 +156,10 @@ }, "dependencies": { "@babel/runtime": "^7.3.4", + "@date-io/date-fns": "1", "@material-ui/core": "4.12.3", "@material-ui/lab": "^4.0.0-alpha.61", + "@material-ui/pickers": "3.3.11", "@popperjs/core": "^2.4.4", "chart.js": "3.7.1", "classnames": "^2.2.5", diff --git a/react/DatePicker/Readme.md b/react/DatePicker/Readme.md new file mode 100644 index 0000000000..99f86a7304 --- /dev/null +++ b/react/DatePicker/Readme.md @@ -0,0 +1,16 @@ +### Usage + +```jsx +import React, { useState } from 'react' +import DemoProvider from 'cozy-ui/docs/components/DemoProvider' +import Variants from 'cozy-ui/docs/components/Variants' +import DatePicker from 'cozy-ui/transpiled/react/DatePicker' + +; +const [selectedDate, setSelectedDate] = useState(null); + + + + + +``` diff --git a/react/DatePicker/index.jsx b/react/DatePicker/index.jsx new file mode 100644 index 0000000000..35dc8cd297 --- /dev/null +++ b/react/DatePicker/index.jsx @@ -0,0 +1,253 @@ +import DateFnsUtils from '@date-io/date-fns' +import { + MuiPickersUtilsProvider, + KeyboardDatePicker +} from '@material-ui/pickers' +import cx from 'classnames' +import formatFNS from 'date-fns/format' +import isBefore from 'date-fns/isBefore' +import LocaleEN from 'date-fns/locale/en-US' +import LocaleFR from 'date-fns/locale/fr' +import subDays from 'date-fns/subDays' +import PropTypes from 'prop-types' +import React, { forwardRef, useState } from 'react' + +import withOwnLocales from './locales/withOwnLocales' +import useBreakpoints from '../providers/Breakpoints' +import { useI18n } from '../providers/I18n' +import { makeStyles } from '../styles' + +const localesFNS = { + fr: LocaleFR, + en: LocaleEN +} + +const useStyles = makeStyles(() => ({ + overrides: { + width: '100%', + height: isDesktop => (isDesktop ? '5rem' : 'inehrit'), + MuiOutlinedInput: { + '&:focused': { + notchedOutline: { + borderColor: 'var(--primaryColor)' + } + } + } + } +})) + +const DatePicker = forwardRef( + ( + { + className, + label, + hasClearButton = false, + value = null, + placeholder, + isValid, + onFocus, + onBlur, + onChange, + minDate, + minDateLabelError, + format, + cancelLabel, + clearLabel, + okLabel, + todayLabel, + showTodayButton = false, + helperText, + errorLabel, + inputVariant = 'outlined', + inputProps, + KeyboardButtonProps + }, + ref + ) => { + const [error, setError] = useState(null) + const [isFocused, setIsFocused] = useState(false) + + const { isDesktop } = useBreakpoints() + const classes = useStyles(isDesktop) + const { t, lang } = useI18n() + + const isError = !isFocused && Boolean(error) + const _helperText = isError ? error : helperText ?? null + const _format = format || (lang === 'fr' ? 'dd/LL/yyyy' : 'LL/dd/yyyy') + const _placeholder = placeholder ?? formatFNS(new Date(), _format) + const _clearLabel = clearLabel || t('clear') + const _todayLabel = todayLabel || t('today') + const _cancelLabel = cancelLabel || t('cancel') + const _okLabel = okLabel || t('ok') + const _minDateLabelError = minDateLabelError + ? minDateLabelError + : minDate + ? t('minDateLabelError', { + date: formatFNS(minDate, _format) + }) + : null + + const _KeyboardButtonProps = { + 'aria-label': label, + ...KeyboardButtonProps + } + const _inputProps = { inputMode: 'numeric', ...inputProps } + + const handleChange = val => { + if (val?.toString() !== 'Invalid Date') { + if (minDate && isBefore(val, subDays(minDate, 1))) { + isValid?.(false) + setError(_minDateLabelError) + return + } + setError(null) + isValid?.(true) + onChange(val) + } else if (val === '') { + setError(null) + isValid?.(true) + onChange(null) + } else { + setError(errorLabel ?? t('invalidDate')) + isValid?.(false) + onChange(val) + } + } + + const handleBlur = () => { + setIsFocused(false) + onFocus?.(true) + onBlur?.(false) + } + const handleFocus = () => { + setIsFocused(true) + onFocus?.(false) + onBlur?.(true) + } + + return ( + + + + ) + } +) + +DatePicker.displayName = 'DatePicker' + +DatePicker.prototype = { + /* + Classname to override the input style + */ + className: PropTypes.string, + /* + Label of the input + */ + label: PropTypes.string, + /* + Value of th input. If set by default with a Date, isValidDate will be false if the value is empty (KeyboardDatePicker behavior) + */ + value: PropTypes.string.isRequired, + /* + Placeholder of the input + */ + placeholder: PropTypes.string, + /* + Function that returns if the value of the input is a valid Date + */ + isValid: PropTypes.func, + /* + Function that returns the value of the input + */ + onChange: PropTypes.func.isRequired, + /* + Function that returns if the input is blured + */ + onBlur: PropTypes.func, + /* + Function that returns if the input is focused + */ + onFocus: PropTypes.func, + /* + Helper text to display when the input is in error + */ + helperText: PropTypes.string, + /* + Min date selectable with the date picker (exclusive) + */ + minDate: PropTypes.instanceOf(Date), + /* + Error message when the min date is not respected + */ + minDateLabelError: PropTypes.string, + /* + Format of the date + */ + format: PropTypes.string, + /* + Date picker cancellation label + */ + cancelLabel: PropTypes.string, + /* + Show today button + */ + showTodayButton: PropTypes.bool, + /* + Date picker today label + */ + todayLabel: PropTypes.string, + /* + Date picker ok label + */ + okLabel: PropTypes.string, + /* + Show the clear button + */ + hasClearButton: PropTypes.bool, + /* + Date picker clear label + */ + clearLabel: PropTypes.string, + /* + Error message when the date is invalid + */ + errorLabel: PropTypes.string, + /* + Variant of the input + */ + inputVariant: PropTypes.string, + /* + Props to override the input + */ + inputProps: PropTypes.object, + /* + Props to override the keyboard button + */ + KeyboardButtonProps: PropTypes.object +} + +export default withOwnLocales(React.memo(DatePicker)) diff --git a/react/DatePicker/locales/en.json b/react/DatePicker/locales/en.json new file mode 100644 index 0000000000..458bacb444 --- /dev/null +++ b/react/DatePicker/locales/en.json @@ -0,0 +1,8 @@ +{ + "cancel": "Cancel", + "clear": "Clear", + "invalidDate": "Invalid date", + "ok": "Ok", + "today": "Today", + "minDateLabelError": "Date should not be before minimal date (%{date})" +} diff --git a/react/DatePicker/locales/fr.json b/react/DatePicker/locales/fr.json new file mode 100644 index 0000000000..43dd14f9d3 --- /dev/null +++ b/react/DatePicker/locales/fr.json @@ -0,0 +1,8 @@ +{ + "cancel": "Annuler", + "clear": "Supprimer", + "invalidDate": "Date invalide", + "ok": "Ok", + "today": "Aujourd'hui", + "minDateLabelError": "La date ne doit pas être antérieure à la date minimale (%{date})" +} diff --git a/react/DatePicker/locales/withOwnLocales.jsx b/react/DatePicker/locales/withOwnLocales.jsx new file mode 100644 index 0000000000..67dbe68a3a --- /dev/null +++ b/react/DatePicker/locales/withOwnLocales.jsx @@ -0,0 +1,10 @@ +import en from './en.json' +import fr from './fr.json' +import withOnlyLocales from '../../providers/I18n/withOnlyLocales' + +export const locales = { + en, + fr +} + +export default withOnlyLocales(locales) diff --git a/react/index.js b/react/index.js index c0582cc1dd..fc8e92e12a 100644 --- a/react/index.js +++ b/react/index.js @@ -139,3 +139,4 @@ export { default as Modal } from './Modal' export { ListSkeleton, ListItemSkeleton } from './Skeletons' export { default as ActionsBar } from './ActionsBar' export { default as Markdown } from './Markdown' +export { default as DatePicker } from './DatePicker' diff --git a/yarn.lock b/yarn.lock index a6b91d5e4f..b35decb9e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1403,7 +1403,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.21.0": +"@babel/runtime@^7.21.0", "@babel/runtime@^7.6.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -1539,6 +1539,18 @@ dependencies: microee "0.0.6" +"@date-io/core@1.x", "@date-io/core@^1.3.13": + version "1.3.13" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" + integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== + +"@date-io/date-fns@1": + version "1.3.13" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-1.3.13.tgz#7798844041640ab393f7e21a7769a65d672f4735" + integrity sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA== + dependencies: + "@date-io/core" "^1.3.13" + "@emotion/cache@^11.0.0", "@emotion/cache@^11.1.3": version "11.1.3" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.1.3.tgz#c7683a9484bcd38d5562f2b9947873cf66829afd" @@ -1910,6 +1922,18 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" +"@material-ui/pickers@3.3.11": + version "3.3.11" + resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.3.11.tgz#dfaaf49955f7bbe3b1c3720293f69dcddeab3ca4" + integrity sha512-pDYjbjUeabapijS2FpSwK/ruJdk7IGeAshpLbKDa3PRRKRy7Nv6sXxAvUg2F+lID/NwUKgBmCYS5bzrl7Xxqzw== + dependencies: + "@babel/runtime" "^7.6.0" + "@date-io/core" "1.x" + "@types/styled-jsx" "^2.2.8" + clsx "^1.0.2" + react-transition-group "^4.0.0" + rifm "^0.7.0" + "@material-ui/styles@^4.11.4": version "4.11.4" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d" @@ -2960,6 +2984,13 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/styled-jsx@^2.2.8": + version "2.2.9" + resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.9.tgz#e50b3f868c055bcbf9bc353eca6c10fdad32a53f" + integrity sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw== + dependencies: + "@types/react" "*" + "@types/tapable@^1": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" @@ -5397,6 +5428,11 @@ clone@^2.1.1: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= +clsx@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + clsx@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" @@ -16315,6 +16351,16 @@ react-test-renderer@16.12.0: react-is "^16.8.6" scheduler "^0.18.0" +react-transition-group@^4.0.0: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-transition-group@^4.3.0, react-transition-group@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" @@ -17355,6 +17401,13 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= +rifm@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" + integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ== + dependencies: + "@babel/runtime" "^7.3.1" + rimraf@2.6.3, rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"