diff --git a/docs/styleguide.config.js b/docs/styleguide.config.js
index ee4c491b6..600f9c0a3 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 d97ad4ee3..d3a188f8c 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 000000000..99f86a730
--- /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 000000000..35dc8cd29
--- /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 000000000..458bacb44
--- /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 000000000..43dd14f9d
--- /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 000000000..67dbe68a3
--- /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 c0582cc1d..fc8e92e12 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 a6b91d5e4..b35decb9e 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"