diff --git a/.eslintrc.js b/.eslintrc.js index 051f32dd561433..9833425ea86db7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,8 +64,7 @@ module.exports = { '!@material-ui/utils/macros', '@material-ui/utils/macros/*', '!@material-ui/utils/macros/*.macro', - // public API: https://next.material-ui-pickers.dev/getting-started/installation#peer-library - '!@material-ui/pickers/adapter/*', + '!@material-ui/lab/dateAdapter/*', ], }, ], diff --git a/.gitignore b/.gitignore index 0f2a98601f9ae1..adcfc918790f40 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .idea .vscode *.log +*.tsbuildinfo /.eslintcache /.nyc_output /benchmark/**/dist diff --git a/docs/next.config.js b/docs/next.config.js index cc54352f5913df..9e87a4a71ea879 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -71,9 +71,7 @@ module.exports = { config.externals = [ (context, request, callback) => { - const hasDependencyOnRepoPackages = ['notistack', '@material-ui/pickers'].includes( - request, - ); + const hasDependencyOnRepoPackages = ['notistack'].includes(request); if (hasDependencyOnRepoPackages) { return callback(null); @@ -108,7 +106,7 @@ module.exports = { // transpile 3rd party packages with dependencies in this repository { test: /\.(js|mjs|jsx)$/, - include: /node_modules(\/|\\)(notistack|@material-ui(\/|\\)pickers)/, + include: /node_modules(\/|\\)notistack/, use: { loader: 'babel-loader', options: { diff --git a/docs/package.json b/docs/package.json index 7d41fd91ffba4b..4a46abb92774db 100644 --- a/docs/package.json +++ b/docs/package.json @@ -32,7 +32,6 @@ "@material-ui/docs": "^5.0.0-alpha.1", "@material-ui/icons": "^5.0.0-alpha.1", "@material-ui/lab": "^5.0.0-alpha.1", - "@material-ui/pickers": "^4.0.0-alpha.11", "@material-ui/styled-engine": "^5.0.0-alpha.1", "@material-ui/styled-engine-sc": "^5.0.0-alpha.1", "@material-ui/styles": "^5.0.0-alpha.1", @@ -69,7 +68,7 @@ "create-emotion-server": "^10.0.27", "cross-env": "^7.0.0", "css-mediaquery": "^0.1.2", - "date-fns": "^2.15.0", + "date-fns": "^2.0.0", "docsearch.js": "^2.6.3", "doctrine": "^3.0.0", "emotion-theming": "^10.0.27", diff --git a/docs/pages/components/date-picker.js b/docs/pages/components/date-picker.js new file mode 100644 index 00000000000000..a9faa5bc5db5fb --- /dev/null +++ b/docs/pages/components/date-picker.js @@ -0,0 +1,24 @@ +import React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import { prepareMarkdown } from 'docs/src/modules/utils/parseMarkdown'; + +const pageFilename = 'components/date-picker'; +const requireDemo = require.context('docs/src/pages/components/date-picker', false, /\.(js|tsx)$/); +const requireRaw = require.context( + '!raw-loader!../../src/pages/components/date-picker', + false, + /\.(js|md|tsx)$/, +); + +// Run styled-components ref logic +// https://github.com/styled-components/styled-components/pull/2998 +requireDemo.keys().map(requireDemo); + +export default function Page({ demos, docs }) { + return ; +} + +Page.getInitialProps = () => { + const { demos, docs } = prepareMarkdown({ pageFilename, requireRaw }); + return { demos, docs }; +}; diff --git a/docs/pages/components/date-range-picker.js b/docs/pages/components/date-range-picker.js new file mode 100644 index 00000000000000..fdea211ea31c9d --- /dev/null +++ b/docs/pages/components/date-range-picker.js @@ -0,0 +1,28 @@ +import React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import { prepareMarkdown } from 'docs/src/modules/utils/parseMarkdown'; + +const pageFilename = 'components/date-range-picker'; +const requireDemo = require.context( + 'docs/src/pages/components/date-range-picker', + false, + /\.(js|tsx)$/, +); +const requireRaw = require.context( + '!raw-loader!../../src/pages/components/date-range-picker', + false, + /\.(js|md|tsx)$/, +); + +// Run styled-components ref logic +// https://github.com/styled-components/styled-components/pull/2998 +requireDemo.keys().map(requireDemo); + +export default function Page({ demos, docs }) { + return ; +} + +Page.getInitialProps = () => { + const { demos, docs } = prepareMarkdown({ pageFilename, requireRaw }); + return { demos, docs }; +}; diff --git a/docs/pages/components/date-time-picker.js b/docs/pages/components/date-time-picker.js new file mode 100644 index 00000000000000..d76dfa6060f970 --- /dev/null +++ b/docs/pages/components/date-time-picker.js @@ -0,0 +1,28 @@ +import React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import { prepareMarkdown } from 'docs/src/modules/utils/parseMarkdown'; + +const pageFilename = 'components/date-time-picker'; +const requireDemo = require.context( + 'docs/src/pages/components/date-time-picker', + false, + /\.(js|tsx)$/, +); +const requireRaw = require.context( + '!raw-loader!../../src/pages/components/date-time-picker', + false, + /\.(js|md|tsx)$/, +); + +// Run styled-components ref logic +// https://github.com/styled-components/styled-components/pull/2998 +requireDemo.keys().map(requireDemo); + +export default function Page({ demos, docs }) { + return ; +} + +Page.getInitialProps = () => { + const { demos, docs } = prepareMarkdown({ pageFilename, requireRaw }); + return { demos, docs }; +}; diff --git a/docs/pages/components/time-picker.js b/docs/pages/components/time-picker.js new file mode 100644 index 00000000000000..c474df51e50e68 --- /dev/null +++ b/docs/pages/components/time-picker.js @@ -0,0 +1,24 @@ +import React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import { prepareMarkdown } from 'docs/src/modules/utils/parseMarkdown'; + +const pageFilename = 'components/time-picker'; +const requireDemo = require.context('docs/src/pages/components/time-picker', false, /\.(js|tsx)$/); +const requireRaw = require.context( + '!raw-loader!../../src/pages/components/time-picker', + false, + /\.(js|md|tsx)$/, +); + +// Run styled-components ref logic +// https://github.com/styled-components/styled-components/pull/2998 +requireDemo.keys().map(requireDemo); + +export default function Page({ demos, docs }) { + return ; +} + +Page.getInitialProps = () => { + const { demos, docs } = prepareMarkdown({ pageFilename, requireRaw }); + return { demos, docs }; +}; diff --git a/docs/scripts/buildApi.ts b/docs/scripts/buildApi.ts index d1d57b32b128af..b54cef27522f61 100644 --- a/docs/scripts/buildApi.ts +++ b/docs/scripts/buildApi.ts @@ -281,6 +281,10 @@ async function buildDocs(options: { prettierConfigPath, theme, } = options; + if (componentObject.filename.indexOf('internal') !== -1) { + return; + } + const src = readFileSync(componentObject.filename, 'utf8'); if (src.match(/@ignore - internal component\./) || src.match(/@ignore - do not document\./)) { diff --git a/docs/src/modules/utils/helpers.js b/docs/src/modules/utils/helpers.js index ca337f20c312e3..5914d7bdd1f44c 100644 --- a/docs/src/modules/utils/helpers.js +++ b/docs/src/modules/utils/helpers.js @@ -83,7 +83,6 @@ function includePeerDependencies(deps, versions) { if ( deps['@material-ui/lab'] || - deps['@material-ui/pickers'] || deps['@material-ui/x'] || deps['@material-ui/x-grid'] || deps['@material-ui/x-pickers'] || @@ -98,10 +97,6 @@ function includePeerDependencies(deps, versions) { deps['@material-ui/icons'] = versions['@material-ui/icons']; deps['@material-ui/lab'] = versions['@material-ui/lab']; } - - if (deps['@material-ui/pickers']) { - deps['date-fns'] = 'latest'; - } } /** @@ -131,8 +126,10 @@ function getDependencies(raw, options = {}) { const deps = {}; const versions = { - 'react-dom': reactVersion, react: reactVersion, + 'react-dom': reactVersion, + '@emotion/core': 'latest', + '@emotion/styled': 'latest', '@material-ui/core': getMuiPackageVersion('core', muiCommitRef), '@material-ui/icons': getMuiPackageVersion('icons', muiCommitRef), '@material-ui/lab': getMuiPackageVersion('lab', muiCommitRef), @@ -142,9 +139,6 @@ function getDependencies(raw, options = {}) { '@material-ui/system': getMuiPackageVersion('system', muiCommitRef), '@material-ui/unstyled': getMuiPackageVersion('unstyled', muiCommitRef), '@material-ui/utils': getMuiPackageVersion('utils', muiCommitRef), - '@material-ui/pickers': 'next', - '@emotion/core': 'latest', - '@emotion/styled': 'latest', }; const re = /^import\s'([^']+)'|import\s[\s\S]*?\sfrom\s+'([^']+)/gm; @@ -164,6 +158,12 @@ function getDependencies(raw, options = {}) { if (!deps[name]) { deps[name] = versions[name] ? versions[name] : 'latest'; } + + // e.g date-fns + const dateAdapter = /^@material-ui\/lab\/dateAdapter\/(.*)/; + if (dateAdapter.test(m[2])) { + deps[dateAdapter.exec(m[2])[1]] = 'latest'; + } } includePeerDependencies(deps, versions); diff --git a/docs/src/modules/utils/helpers.test.js b/docs/src/modules/utils/helpers.test.js index 80152e4c1918f4..71cda164298c27 100644 --- a/docs/src/modules/utils/helpers.test.js +++ b/docs/src/modules/utils/helpers.test.js @@ -22,13 +22,13 @@ const styles = theme => ({ it('should handle @ dependencies', () => { expect(getDependencies(s1)).to.deep.equal({ + react: 'latest', + 'react-dom': 'latest', '@emotion/core': 'latest', '@emotion/styled': 'latest', '@foo-bar/bip': 'latest', '@material-ui/core': 'next', 'prop-types': 'latest', - 'react-dom': 'latest', - react: 'latest', }); }); @@ -48,6 +48,8 @@ const suggestions = [ `; expect(getDependencies(source)).to.deep.equal({ + react: 'latest', + 'react-dom': 'latest', '@emotion/core': 'latest', '@emotion/styled': 'latest', '@material-ui/core': 'next', @@ -55,20 +57,18 @@ const suggestions = [ 'autosuggest-highlight': 'latest', 'prop-types': 'latest', 'react-draggable': 'latest', - 'react-dom': 'latest', - react: 'latest', }); }); it('should support next dependencies', () => { expect(getDependencies(s1, { reactVersion: 'next' })).to.deep.equal({ + react: 'next', + 'react-dom': 'next', '@emotion/core': 'latest', '@emotion/styled': 'latest', '@foo-bar/bip': 'latest', '@material-ui/core': 'next', 'prop-types': 'latest', - 'react-dom': 'next', - react: 'next', }); }); @@ -78,31 +78,31 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import Grid from '@material-ui/core/Grid'; import { withStyles } from '@material-ui/core/styles'; -import DateFnsAdapter from "@material-ui/pickers/adapter/date-fns"; -import { LocalizationProvider as MuiPickersLocalizationProvider, KeyboardTimePicker, KeyboardDatePicker } from '@material-ui/pickers'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import { LocalizationProvider as MuiPickersLocalizationProvider, KeyboardTimePicker, KeyboardDatePicker } from '@material-ui/lab'; `; expect(getDependencies(source)).to.deep.equal({ - 'date-fns': 'latest', + react: 'latest', + 'react-dom': 'latest', + 'prop-types': 'latest', '@emotion/core': 'latest', '@emotion/styled': 'latest', - '@material-ui/pickers': 'next', '@material-ui/core': 'next', - 'prop-types': 'latest', - 'react-dom': 'latest', - react: 'latest', + '@material-ui/lab': 'next', + 'date-fns': 'latest', }); }); it('can collect required @types packages', () => { expect(getDependencies(s1, { codeLanguage: 'TS' })).to.deep.equal({ + react: 'latest', + 'react-dom': 'latest', + 'prop-types': 'latest', '@emotion/core': 'latest', '@emotion/styled': 'latest', '@foo-bar/bip': 'latest', '@material-ui/core': 'next', - 'prop-types': 'latest', - 'react-dom': 'latest', - react: 'latest', '@types/foo-bar__bip': 'latest', '@types/prop-types': 'latest', '@types/react-dom': 'latest', @@ -114,22 +114,22 @@ import { LocalizationProvider as MuiPickersLocalizationProvider, KeyboardTimePic it('should handle multilines', () => { const source = ` import * as React from 'react'; -import DateFnsAdapter from '@material-ui/pickers/adapter/date-fns'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; import { LocalizationProvider as MuiPickersLocalizationProvider, KeyboardTimePicker, KeyboardDatePicker, -} from '@material-ui/pickers'; +} from '@material-ui/lab'; `; expect(getDependencies(source)).to.deep.equal({ - 'date-fns': 'latest', + react: 'latest', + 'react-dom': 'latest', '@emotion/core': 'latest', '@emotion/styled': 'latest', '@material-ui/core': 'next', - '@material-ui/pickers': 'next', - react: 'latest', - 'react-dom': 'latest', + '@material-ui/lab': 'next', + 'date-fns': 'latest', }); }); @@ -139,12 +139,12 @@ import lab from '@material-ui/lab'; `; expect(getDependencies(source)).to.deep.equal({ + react: 'latest', + 'react-dom': 'latest', '@emotion/core': 'latest', '@emotion/styled': 'latest', '@material-ui/core': 'next', '@material-ui/lab': 'next', - react: 'latest', - 'react-dom': 'latest', }); }); @@ -156,6 +156,8 @@ import { useDemoData } from '@material-ui/x-grid-data-generator'; `; expect(getDependencies(source, { codeLanguage: 'TS' })).to.deep.equal({ + react: 'latest', + 'react-dom': 'latest', '@emotion/core': 'latest', '@emotion/styled': 'latest', '@material-ui/core': 'next', @@ -165,8 +167,6 @@ import { useDemoData } from '@material-ui/x-grid-data-generator'; '@material-ui/x-grid-data-generator': 'latest', '@types/react': 'latest', '@types/react-dom': 'latest', - react: 'latest', - 'react-dom': 'latest', typescript: 'latest', }); }); diff --git a/docs/src/pages.js b/docs/src/pages.js index 0abcbefb7047d5..dd1b74621197f9 100644 --- a/docs/src/pages.js +++ b/docs/src/pages.js @@ -38,7 +38,6 @@ const pages = [ { pathname: '/components/button-group' }, { pathname: '/components/checkboxes', title: 'Checkbox' }, { pathname: '/components/floating-action-button' }, - { pathname: '/components/pickers', title: 'Date / Time' }, { pathname: '/components/radio-buttons', title: 'Radio button' }, { pathname: '/components/rating' }, { pathname: '/components/selects', title: 'Select' }, @@ -145,6 +144,18 @@ const pages = [ subheader: '/components/lab', children: [ { pathname: '/components/about-the-lab', title: 'About the lab 🧪' }, + { + pathname: '/components', + subheader: '/components/lab-pickers', + title: 'Date / Time', + children: [ + { pathname: '/components/pickers', title: 'Introduction' }, + { pathname: '/components/date-picker' }, + { pathname: '/components/date-range-picker' }, + { pathname: '/components/date-time-picker' }, + { pathname: '/components/time-picker' }, + ], + }, { pathname: '/components/slider-styled' }, { pathname: '/components/timeline' }, { pathname: '/components/trap-focus' }, diff --git a/docs/src/pages/components/date-picker/BasicDatePicker.js b/docs/src/pages/components/date-picker/BasicDatePicker.js new file mode 100644 index 00000000000000..264282a26d1890 --- /dev/null +++ b/docs/src/pages/components/date-picker/BasicDatePicker.js @@ -0,0 +1,22 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DatePicker from '@material-ui/lab/DatePicker'; + +export default function BasicDatePicker() { + const [value, setValue] = React.useState(null); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/BasicDatePicker.tsx b/docs/src/pages/components/date-picker/BasicDatePicker.tsx new file mode 100644 index 00000000000000..40ad4418a997c4 --- /dev/null +++ b/docs/src/pages/components/date-picker/BasicDatePicker.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DatePicker from '@material-ui/lab/DatePicker'; + +export default function BasicDatePicker() { + const [value, setValue] = React.useState(null); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/CustomDay.js b/docs/src/pages/components/date-picker/CustomDay.js new file mode 100644 index 00000000000000..a4007731f04b27 --- /dev/null +++ b/docs/src/pages/components/date-picker/CustomDay.js @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { makeStyles } from '@material-ui/core'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import DatePicker from '@material-ui/lab/DatePicker'; +import PickersDay from '@material-ui/lab/PickersDay'; +import clsx from 'clsx'; +import endOfWeek from 'date-fns/endOfWeek'; +import isSameDay from 'date-fns/isSameDay'; +import isWithinInterval from 'date-fns/isWithinInterval'; +import startOfWeek from 'date-fns/startOfWeek'; + +const useStyles = makeStyles((theme) => ({ + highlight: { + borderRadius: 0, + backgroundColor: theme.palette.primary.main, + color: theme.palette.common.white, + '&:hover, &:focus': { + backgroundColor: theme.palette.primary.dark, + }, + }, + firstHighlight: { + borderTopLeftRadius: '50%', + borderBottomLeftRadius: '50%', + }, + endHighlight: { + borderTopRightRadius: '50%', + borderBottomRightRadius: '50%', + }, +})); + +export default function CustomDay() { + const classes = useStyles(); + const [selectedDate, handleDateChange] = React.useState(new Date()); + + const renderWeekPickerDay = ( + date, + _selectedDates, + PickersDayComponentProps, + ) => { + if (!selectedDate) { + return ; + } + + const start = startOfWeek(selectedDate); + const end = endOfWeek(selectedDate); + + const dayIsBetween = isWithinInterval(date, { start, end }); + const isFirstDay = isSameDay(date, start); + const isLastDay = isSameDay(date, end); + + return ( + + ); + }; + + return ( + + } + inputFormat="'Week of' MMM d" + /> + + ); +} diff --git a/packages/pickers/docs/pages/demo/datepicker/CustomDay.example.tsx b/docs/src/pages/components/date-picker/CustomDay.tsx similarity index 50% rename from packages/pickers/docs/pages/demo/datepicker/CustomDay.example.tsx rename to docs/src/pages/components/date-picker/CustomDay.tsx index 47bd1ea269efc1..7717868d0e8ec2 100644 --- a/packages/pickers/docs/pages/demo/datepicker/CustomDay.example.tsx +++ b/docs/src/pages/components/date-picker/CustomDay.tsx @@ -1,16 +1,15 @@ -/* eslint-disable no-underscore-dangle */ import * as React from 'react'; +import { makeStyles } from '@material-ui/core'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import DatePicker from '@material-ui/lab/DatePicker'; +import PickersDay, { PickersDayProps } from '@material-ui/lab/PickersDay'; import clsx from 'clsx'; -import isSameDay from 'date-fns/isSameDay'; import endOfWeek from 'date-fns/endOfWeek'; -import startOfWeek from 'date-fns/startOfWeek'; -import TextField from '@material-ui/core/TextField'; +import isSameDay from 'date-fns/isSameDay'; import isWithinInterval from 'date-fns/isWithinInterval'; -import { makeStyles } from '@material-ui/core'; -// this guy required only on the docs site to work with dynamic date library -import { DatePicker, PickersDay, PickersDayProps } from '@material-ui/pickers'; -// TODO remove relative import -import { makeJSDateObject } from '../../../utils/helpers'; +import startOfWeek from 'date-fns/startOfWeek'; const useStyles = makeStyles((theme) => ({ highlight: { @@ -31,28 +30,31 @@ const useStyles = makeStyles((theme) => ({ }, })); -export default function CustomDay(demoProps: any) { +export default function CustomDay() { const classes = useStyles(); - const [selectedDate, handleDateChange] = React.useState(new Date()); + const [selectedDate, handleDateChange] = React.useState( + new Date(), + ); const renderWeekPickerDay = ( date: Date, _selectedDates: Date[], - DayComponentProps: PickersDayProps + PickersDayComponentProps: PickersDayProps, ) => { - const dateClone = makeJSDateObject(date); - const selectedDateClone = makeJSDateObject(selectedDate ?? new Date()); + if (!selectedDate) { + return ; + } - const start = startOfWeek(selectedDateClone); - const end = endOfWeek(selectedDateClone); + const start = startOfWeek(selectedDate); + const end = endOfWeek(selectedDate); - const dayIsBetween = isWithinInterval(dateClone, { start, end }); - const isFirstDay = isSameDay(dateClone, start); - const isLastDay = isSameDay(dateClone, end); + const dayIsBetween = isWithinInterval(date, { start, end }); + const isFirstDay = isSameDay(date, start); + const isLastDay = isSameDay(date, end); return ( } - inputFormat={demoProps.__willBeReplacedGetFormatString({ - moment: `[Week of] MMM D`, - dateFns: "'Week of' MMM d", - })} - /> + + } + inputFormat="'Week of' MMM d" + /> + ); } diff --git a/docs/src/pages/components/date-picker/CustomInput.js b/docs/src/pages/components/date-picker/CustomInput.js new file mode 100644 index 00000000000000..62f6ec8116f184 --- /dev/null +++ b/docs/src/pages/components/date-picker/CustomInput.js @@ -0,0 +1,27 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import DesktopDatePicker from '@material-ui/lab/DatePicker'; + +export default function CustomInput() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={({ inputRef, inputProps, InputProps }) => ( + + + {InputProps?.endAdornment} + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/CustomInput.tsx b/docs/src/pages/components/date-picker/CustomInput.tsx new file mode 100644 index 00000000000000..4450f2abe79c54 --- /dev/null +++ b/docs/src/pages/components/date-picker/CustomInput.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import DesktopDatePicker from '@material-ui/lab/DatePicker'; + +export default function CustomInput() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={({ inputRef, inputProps, InputProps }) => ( + + + {InputProps?.endAdornment} + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/InternalPickers.js b/docs/src/pages/components/date-picker/InternalPickers.js new file mode 100644 index 00000000000000..dc630d6404d03e --- /dev/null +++ b/docs/src/pages/components/date-picker/InternalPickers.js @@ -0,0 +1,18 @@ +import * as React from 'react'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import DayPicker from '@material-ui/lab/DayPicker'; + +export default function InternalPickers() { + const [date, setDate] = React.useState(new Date()); + + return ( + + setDate(newValue)} + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/InternalPickers.tsx b/docs/src/pages/components/date-picker/InternalPickers.tsx new file mode 100644 index 00000000000000..b2a7f16c48cd0b --- /dev/null +++ b/docs/src/pages/components/date-picker/InternalPickers.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import DayPicker from '@material-ui/lab/DayPicker'; + +export default function InternalPickers() { + const [date, setDate] = React.useState(new Date()); + + return ( + + setDate(newValue)} + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/LocalizedDatePicker.js b/docs/src/pages/components/date-picker/LocalizedDatePicker.js new file mode 100644 index 00000000000000..47c9b5eba21a41 --- /dev/null +++ b/docs/src/pages/components/date-picker/LocalizedDatePicker.js @@ -0,0 +1,61 @@ +import * as React from 'react'; +import frLocale from 'date-fns/locale/fr'; +import ruLocale from 'date-fns/locale/ru'; +import deLocale from 'date-fns/locale/de'; +import enLocale from 'date-fns/locale/en-US'; +import ToggleButton from '@material-ui/core/ToggleButton'; +import ToggleButtonGroup from '@material-ui/core/ToggleButtonGroup'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import DatePicker from '@material-ui/lab/DatePicker'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; + +const localeMap = { + en: enLocale, + fr: frLocale, + ru: ruLocale, + de: deLocale, +}; + +const maskMap = { + fr: '__/__/____', + en: '__/__/____', + ru: '__.__.____', + de: '__.__.____', +}; + +export default function LocalizedDatePicker() { + const [locale, setLocale] = React.useState('ru'); + const [selectedDate, handleDateChange] = React.useState(new Date()); + + const selectLocale = (newLocale) => { + setLocale(newLocale); + }; + + return ( + +
+ handleDateChange(date)} + renderInput={(params) => } + /> + + {Object.keys(localeMap).map((localeItem) => ( + selectLocale(localeItem)} + > + {localeItem} + + ))} + +
+
+ ); +} diff --git a/docs/src/pages/components/date-picker/LocalizedDatePicker.tsx b/docs/src/pages/components/date-picker/LocalizedDatePicker.tsx new file mode 100644 index 00000000000000..d077d0660b1329 --- /dev/null +++ b/docs/src/pages/components/date-picker/LocalizedDatePicker.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import frLocale from 'date-fns/locale/fr'; +import ruLocale from 'date-fns/locale/ru'; +import deLocale from 'date-fns/locale/de'; +import enLocale from 'date-fns/locale/en-US'; +import ToggleButton from '@material-ui/core/ToggleButton'; +import ToggleButtonGroup from '@material-ui/core/ToggleButtonGroup'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import DatePicker from '@material-ui/lab/DatePicker'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; + +const localeMap = { + en: enLocale, + fr: frLocale, + ru: ruLocale, + de: deLocale, +}; + +const maskMap = { + fr: '__/__/____', + en: '__/__/____', + ru: '__.__.____', + de: '__.__.____', +}; + +export default function LocalizedDatePicker() { + const [locale, setLocale] = React.useState('ru'); + const [selectedDate, handleDateChange] = React.useState( + new Date(), + ); + + const selectLocale = (newLocale: any) => { + setLocale(newLocale); + }; + + return ( + +
+ handleDateChange(date)} + renderInput={(params) => } + /> + + {Object.keys(localeMap).map((localeItem) => ( + selectLocale(localeItem)} + > + {localeItem} + + ))} + +
+
+ ); +} diff --git a/docs/src/pages/components/date-picker/ResponsiveDatePickers.js b/docs/src/pages/components/date-picker/ResponsiveDatePickers.js new file mode 100644 index 00000000000000..30aa27aac43a08 --- /dev/null +++ b/docs/src/pages/components/date-picker/ResponsiveDatePickers.js @@ -0,0 +1,46 @@ +import React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DatePicker from '@material-ui/lab/DatePicker'; +import MobileDatePicker from '@material-ui/lab/MobileDatePicker'; +import DesktopDatePicker from '@material-ui/lab/DesktopDatePicker'; + +export default function ResponsiveDatePickers() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-picker/ResponsiveDatePickers.tsx b/docs/src/pages/components/date-picker/ResponsiveDatePickers.tsx new file mode 100644 index 00000000000000..09c17ad9e74d0b --- /dev/null +++ b/docs/src/pages/components/date-picker/ResponsiveDatePickers.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DatePicker from '@material-ui/lab/DatePicker'; +import MobileDatePicker from '@material-ui/lab/MobileDatePicker'; +import DesktopDatePicker from '@material-ui/lab/DesktopDatePicker'; + +export default function ResponsiveDatePickers() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-picker/ServerRequestDatePicker.js b/docs/src/pages/components/date-picker/ServerRequestDatePicker.js new file mode 100644 index 00000000000000..477e6635c62010 --- /dev/null +++ b/docs/src/pages/components/date-picker/ServerRequestDatePicker.js @@ -0,0 +1,106 @@ +import * as React from 'react'; +import Badge from '@material-ui/core/Badge'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import PickersDay from '@material-ui/lab/PickersDay'; +import DatePicker from '@material-ui/lab/DatePicker'; +import PickersCalendarSkeleton from '@material-ui/lab/PickersCalendarSkeleton'; +import getDaysInMonth from 'date-fns/getDaysInMonth'; + +function getRandomNumber(min, max) { + return Math.round(Math.random() * (max - min) + min); +} + +/** + * Mimic fetch with abort controller https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort + * ⚠️ No IE11 support + */ +function fakeFetch(date, { signal }) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + const daysInMonth = getDaysInMonth(date); + const daysToHighlight = [1, 2, 3].map(() => + getRandomNumber(1, daysInMonth), + ); + + resolve({ daysToHighlight }); + }, 500); + + signal.onabort = () => { + clearTimeout(timeout); + reject(new Error('aborted')); + }; + }); +} + +const initialValue = new Date(); + +export default function ServerRequestDatePicker() { + const requestAbortController = React.useRef(null); + const [isLoading, setIsLoading] = React.useState(false); + const [highlightedDays, setHighlightedDays] = React.useState([1, 2, 15]); + const [value, setValue] = React.useState(initialValue); + + const fetchHighlightedDays = (date) => { + const controller = new AbortController(); + fakeFetch(date, { + signal: controller.signal, + }) + .then(({ daysToHighlight }) => { + setHighlightedDays(daysToHighlight); + setIsLoading(false); + }) + .catch(() => console.log('Wow, you are switching months too quickly 🐕')); + + requestAbortController.current = controller; + }; + + React.useEffect(() => { + fetchHighlightedDays(initialValue); + // abort request on unmount + return () => requestAbortController.current?.abort(); + }, []); + + const handleMonthChange = (date) => { + if (requestAbortController.current) { + // make sure that you are aborting useless requests + // because it is possible to switch between months pretty quickly + requestAbortController.current.abort(); + } + + setIsLoading(true); + setHighlightedDays([]); + fetchHighlightedDays(date); + }; + + return ( + + { + setValue(newValue); + }} + onMonthChange={handleMonthChange} + renderInput={(params) => } + renderLoading={() => } + renderDay={(day, _value, DayComponentProps) => { + const isSelected = + !DayComponentProps.outsideCurrentMonth && + highlightedDays.indexOf(day.getDate()) > 0; + + return ( + + + + ); + }} + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/ServerRequestDatePicker.tsx b/docs/src/pages/components/date-picker/ServerRequestDatePicker.tsx new file mode 100644 index 00000000000000..295f98c649cd1b --- /dev/null +++ b/docs/src/pages/components/date-picker/ServerRequestDatePicker.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import Badge from '@material-ui/core/Badge'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import PickersDay from '@material-ui/lab/PickersDay'; +import DatePicker from '@material-ui/lab/DatePicker'; +import PickersCalendarSkeleton from '@material-ui/lab/PickersCalendarSkeleton'; +import getDaysInMonth from 'date-fns/getDaysInMonth'; + +function getRandomNumber(min: number, max: number) { + return Math.round(Math.random() * (max - min) + min); +} + +/** + * Mimic fetch with abort controller https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort + * ⚠️ No IE11 support + */ +function fakeFetch(date: Date, { signal }: { signal: AbortSignal }) { + return new Promise<{ daysToHighlight: number[] }>((resolve, reject) => { + const timeout = setTimeout(() => { + const daysInMonth = getDaysInMonth(date); + const daysToHighlight = [1, 2, 3].map(() => + getRandomNumber(1, daysInMonth), + ); + + resolve({ daysToHighlight }); + }, 500); + + signal.onabort = () => { + clearTimeout(timeout); + reject(new Error('aborted')); + }; + }); +} + +const initialValue = new Date(); + +export default function ServerRequestDatePicker() { + const requestAbortController = React.useRef(null); + const [isLoading, setIsLoading] = React.useState(false); + const [highlightedDays, setHighlightedDays] = React.useState([1, 2, 15]); + const [value, setValue] = React.useState(initialValue); + + const fetchHighlightedDays = (date: Date) => { + const controller = new AbortController(); + fakeFetch(date, { + signal: controller.signal, + }) + .then(({ daysToHighlight }) => { + setHighlightedDays(daysToHighlight); + setIsLoading(false); + }) + .catch(() => console.log('Wow, you are switching months too quickly 🐕')); + + requestAbortController.current = controller; + }; + + React.useEffect(() => { + fetchHighlightedDays(initialValue); + // abort request on unmount + return () => requestAbortController.current?.abort(); + }, []); + + const handleMonthChange = (date: Date) => { + if (requestAbortController.current) { + // make sure that you are aborting useless requests + // because it is possible to switch between months pretty quickly + requestAbortController.current.abort(); + } + + setIsLoading(true); + setHighlightedDays([]); + fetchHighlightedDays(date); + }; + + return ( + + { + setValue(newValue); + }} + onMonthChange={handleMonthChange} + renderInput={(params) => } + renderLoading={() => } + renderDay={(day, _value, DayComponentProps) => { + const isSelected = + !DayComponentProps.outsideCurrentMonth && + highlightedDays.indexOf(day.getDate()) > 0; + + return ( + + + + ); + }} + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/StaticDatePickerDemo.js b/docs/src/pages/components/date-picker/StaticDatePickerDemo.js new file mode 100644 index 00000000000000..8672cf992393fc --- /dev/null +++ b/docs/src/pages/components/date-picker/StaticDatePickerDemo.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import StaticDatePicker from '@material-ui/lab/StaticDatePicker'; + +export default function StaticDatePickerDemo() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/StaticDatePickerDemo.tsx b/docs/src/pages/components/date-picker/StaticDatePickerDemo.tsx new file mode 100644 index 00000000000000..0afc3390aee734 --- /dev/null +++ b/docs/src/pages/components/date-picker/StaticDatePickerDemo.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import StaticDatePicker from '@material-ui/lab/StaticDatePicker'; + +export default function StaticDatePickerDemo() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/StaticDatePickerLandscape.js b/docs/src/pages/components/date-picker/StaticDatePickerLandscape.js new file mode 100644 index 00000000000000..16022e1cf500b1 --- /dev/null +++ b/docs/src/pages/components/date-picker/StaticDatePickerLandscape.js @@ -0,0 +1,25 @@ +import * as React from 'react'; +import isWeekend from 'date-fns/isWeekend'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import StaticDatePicker from '@material-ui/lab/StaticDatePicker'; + +export default function StaticDatePickerLandscape() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/StaticDatePickerLandscape.tsx b/docs/src/pages/components/date-picker/StaticDatePickerLandscape.tsx new file mode 100644 index 00000000000000..b7fd17a98a51c3 --- /dev/null +++ b/docs/src/pages/components/date-picker/StaticDatePickerLandscape.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import isWeekend from 'date-fns/isWeekend'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import StaticDatePicker from '@material-ui/lab/StaticDatePicker'; + +export default function StaticDatePickerLandscape() { + const [value, setValue] = React.useState(new Date()); + + return ( + + + orientation="landscape" + openTo="date" + value={value} + shouldDisableDate={isWeekend} + onChange={(newValue) => { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/date-picker/ViewsDatePicker.js b/docs/src/pages/components/date-picker/ViewsDatePicker.js new file mode 100644 index 00000000000000..0297d95edb10f0 --- /dev/null +++ b/docs/src/pages/components/date-picker/ViewsDatePicker.js @@ -0,0 +1,74 @@ +import React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import DatePicker from '@material-ui/lab/DatePicker'; + +export default function ViewsDatePicker() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-picker/ViewsDatePicker.tsx b/docs/src/pages/components/date-picker/ViewsDatePicker.tsx new file mode 100644 index 00000000000000..bc53ca7dfdfbbd --- /dev/null +++ b/docs/src/pages/components/date-picker/ViewsDatePicker.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizaitonProvider from '@material-ui/lab/LocalizationProvider'; +import DatePicker from '@material-ui/lab/DatePicker'; + +export default function ViewsDatePicker() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + renderInput={(params) => ( + + )} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-picker/date-picker.md b/docs/src/pages/components/date-picker/date-picker.md new file mode 100644 index 00000000000000..58f1b2347a0355 --- /dev/null +++ b/docs/src/pages/components/date-picker/date-picker.md @@ -0,0 +1,105 @@ +--- +title: React Date Picker component +components: DatePicker, PickersDay +githubLabel: 'component: DatePicker' +packageName: '@material-ui/lab' +materialDesign: https://material.io/components/date-pickers +--- + +# Date Picker + +

Date pickers let the user select a date.

+ +Date pickers let the user select a date. Date pickers are displayed with: + +- Dialogs on mobile +- Text field dropdowns on desktop + +{{"component": "modules/components/ComponentLinkHeader.js"}} + +## Requirements + +This component relies on the date management library of your choice. It supports [date-fns](https://date-fns.org/), [luxon](https://moment.github.io/luxon/), [dayjs](https://github.com/iamkun/dayjs), [moment](https://momentjs.com/) and any other library via a public `dateAdapter` interface. + +Please install any of these libraries and set up the right date engine by wrapping your root (or the highest level you wish the pickers to be available) with `LocalizationProvider`: + +```jsx +// or @material-ui/lab/dateAdapter/{dayjs,luxon,moment} or any valid date-io adapter +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; + +function App() { + return ( + + ... + + ); +} +``` + +## Basic usage + +The date picker will be rendered as a modal dialog on mobile, and a textfield with a popover on desktop. + +{{"demo": "pages/components/date-picker/BasicDatePicker.js"}} + +## Responsiveness + +The date picker component is designed and optimized for the device it runs on. + +- The "Mobile" version works best for touch devices and small screens. +- The "Desktop" version works best for mouse devices and large screens. + +By default, the `DatePicker` component uses a `@media (pointer: fine)` media query to determine which version to use. +This can be customized with the `desktopModeMediaQuery` prop. + +{{"demo": "pages/components/date-picker/ResponsiveDatePickers.js"}} + +## Localization + +Use `LocalizationProvider` to change the date-engine locale that is used to render the date picker. Here is an example of changing the locale for the `date-fns` adapter: + +{{"demo": "pages/components/date-picker/LocalizedDatePicker.js"}} + +## Views playground + +It's possible to combine `year`, `month`, and `date` selection views. Views will appear in the order they're included in the `views` array. + +{{"demo": "pages/components/date-picker/ViewsDatePicker.js"}} + +## Static mode + +It's possible to render any picker without the modal/popover and text field. This can be helpful when building custom popover/modal containers. + +{{"demo": "pages/components/date-picker/StaticDatePickerDemo.js", "bg": true}} + +## Landscape orientation + +For ease of use the date picker will automatically change the layout between portrait and landscape by subscription to the `window.orientation` change. You can force a specific layout using the `orientation` prop. + +{{"demo": "pages/components/date-picker/StaticDatePickerLandscape.js", "bg": true}} + +## Sub-components + +Some lower level sub-components (`DayPicker`, `MonthPicker` and `YearPicker`) are also exported. These are rendered without a wrapper or outer logic (masked input, date values parsing and validation, etc.). + +{{"demo": "pages/components/date-picker/InternalPickers.js"}} + +## Custom input component + +You can customize rendering of the input with the `renderInput` prop. Make sure to spread `ref` and `inputProps` correctly to the custom input component. + +{{"demo": "pages/components/date-picker/CustomInput.js"}} + +## Customized day rendering + +The displayed days are customizable with the `renderDay` function prop. +You can take advantage of the internal [PickersDay](/api/pickers-day) component. + +{{"demo": "pages/components/date-picker/CustomDay.js"}} + +## Dynamic data + +Sometimes it may be necessary to display additional info right in the calendar. Here's an example of prefetching and displaying server-side data using the `onMonthChange`, `loading`, and `renderDay` props. + +{{"demo": "pages/components/date-picker/ServerRequestDatePicker.js"}} diff --git a/docs/src/pages/components/date-range-picker/BasicDateRangePicker.js b/docs/src/pages/components/date-range-picker/BasicDateRangePicker.js new file mode 100644 index 00000000000000..820556c9d393e7 --- /dev/null +++ b/docs/src/pages/components/date-range-picker/BasicDateRangePicker.js @@ -0,0 +1,30 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateRangePicker from '@material-ui/lab/DateRangePicker'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; + +export default function BasicDateRangePicker() { + const [value, setValue] = React.useState([null, null]); + + return ( + + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-range-picker/BasicDateRangePicker.tsx b/docs/src/pages/components/date-range-picker/BasicDateRangePicker.tsx new file mode 100644 index 00000000000000..a41da0dbdf94ab --- /dev/null +++ b/docs/src/pages/components/date-range-picker/BasicDateRangePicker.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateRangePicker, { DateRange } from '@material-ui/lab/DateRangePicker'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; + +export default function BasicDateRangePicker() { + const [value, setValue] = React.useState>([null, null]); + + return ( + + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-range-picker/CalendarsDateRangePicker.js b/docs/src/pages/components/date-range-picker/CalendarsDateRangePicker.js new file mode 100644 index 00000000000000..598cf7a15888b3 --- /dev/null +++ b/docs/src/pages/components/date-range-picker/CalendarsDateRangePicker.js @@ -0,0 +1,64 @@ +import * as React from 'react'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangePicker from '@material-ui/lab/DateRangePicker'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; + +export default function CalendarsDateRangePicker() { + const [value, setValue] = React.useState([null, null]); + + return ( + + + 1 calendar + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + 2 calendars + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + 3 calendars + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + + ); +} diff --git a/docs/src/pages/components/date-range-picker/CalendarsDateRangePicker.tsx b/docs/src/pages/components/date-range-picker/CalendarsDateRangePicker.tsx new file mode 100644 index 00000000000000..ccbb195d05b594 --- /dev/null +++ b/docs/src/pages/components/date-range-picker/CalendarsDateRangePicker.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangePicker, { DateRange } from '@material-ui/lab/DateRangePicker'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; + +export default function CalendarsDateRangePicker() { + const [value, setValue] = React.useState>([null, null]); + + return ( + + + 1 calendar + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + 2 calendars + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + 3 calendars + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + + ); +} diff --git a/docs/src/pages/components/date-range-picker/CustomDateRangeInputs.js b/docs/src/pages/components/date-range-picker/CustomDateRangeInputs.js new file mode 100644 index 00000000000000..96973e426ecba0 --- /dev/null +++ b/docs/src/pages/components/date-range-picker/CustomDateRangeInputs.js @@ -0,0 +1,24 @@ +import * as React from 'react'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangePicker from '@material-ui/lab/DateRangePicker'; + +export default function CustomDateRangeInputs() { + const [selectedDate, handleDateChange] = React.useState([null, null]); + + return ( + + handleDateChange(date)} + renderInput={(startProps, endProps) => ( + + + + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-range-picker/CustomDateRangeInputs.tsx b/docs/src/pages/components/date-range-picker/CustomDateRangeInputs.tsx new file mode 100644 index 00000000000000..8c595e73e35b8c --- /dev/null +++ b/docs/src/pages/components/date-range-picker/CustomDateRangeInputs.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangePicker, { DateRange } from '@material-ui/lab/DateRangePicker'; + +export default function CustomDateRangeInputs() { + const [selectedDate, handleDateChange] = React.useState>([ + null, + null, + ]); + + return ( + + handleDateChange(date)} + renderInput={(startProps, endProps) => ( + + } + {...startProps.inputProps} + /> + } + {...endProps.inputProps} + /> + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-range-picker/MinMaxDateRangePicker.js b/docs/src/pages/components/date-range-picker/MinMaxDateRangePicker.js new file mode 100644 index 00000000000000..75bb9caf8879db --- /dev/null +++ b/docs/src/pages/components/date-range-picker/MinMaxDateRangePicker.js @@ -0,0 +1,35 @@ +import * as React from 'react'; +import addWeeks from 'date-fns/addWeeks'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangePicker from '@material-ui/lab/DateRangePicker'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; + +function getWeeksAfter(date, amount) { + return date ? addWeeks(date, amount) : undefined; +} + +export default function MinMaxDateRangePicker() { + const [value, setValue] = React.useState([null, null]); + + return ( + + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-range-picker/MinMaxDateRangePicker.tsx b/docs/src/pages/components/date-range-picker/MinMaxDateRangePicker.tsx new file mode 100644 index 00000000000000..a3389354d33a15 --- /dev/null +++ b/docs/src/pages/components/date-range-picker/MinMaxDateRangePicker.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import addWeeks from 'date-fns/addWeeks'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangePicker, { DateRange } from '@material-ui/lab/DateRangePicker'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; + +function getWeeksAfter(date: Date | null, amount: number) { + return date ? addWeeks(date, amount) : undefined; +} + +export default function MinMaxDateRangePicker() { + const [value, setValue] = React.useState>([null, null]); + + return ( + + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-range-picker/ResponsiveDateRangePicker.js b/docs/src/pages/components/date-range-picker/ResponsiveDateRangePicker.js new file mode 100644 index 00000000000000..7b19699ad4dc28 --- /dev/null +++ b/docs/src/pages/components/date-range-picker/ResponsiveDateRangePicker.js @@ -0,0 +1,44 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; +import MobileDateRangePicker from '@material-ui/lab/MobileDateRangePicker'; +import DesktopDateRangePicker from '@material-ui/lab/DesktopDateRangePicker'; + +export default function ResponsiveDateRangePicker() { + const [value, setValue] = React.useState([null, null]); + + return ( + + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + ); +} diff --git a/packages/pickers/docs/pages/demo/daterangepicker/ResponsiveDateRangePicker.example.tsx b/docs/src/pages/components/date-range-picker/ResponsiveDateRangePicker.tsx similarity index 60% rename from packages/pickers/docs/pages/demo/daterangepicker/ResponsiveDateRangePicker.example.tsx rename to docs/src/pages/components/date-range-picker/ResponsiveDateRangePicker.tsx index 67cf7f19586791..872aa6b735bce4 100644 --- a/packages/pickers/docs/pages/demo/daterangepicker/ResponsiveDateRangePicker.example.tsx +++ b/docs/src/pages/components/date-range-picker/ResponsiveDateRangePicker.tsx @@ -1,21 +1,24 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import { - MobileDateRangePicker, - DateRangeDelimiter, - DesktopDateRangePicker, +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; +import MobileDateRangePicker, { DateRange, -} from '@material-ui/pickers'; +} from '@material-ui/lab/MobileDateRangePicker'; +import DesktopDateRangePicker from '@material-ui/lab/DesktopDateRangePicker'; export default function ResponsiveDateRangePicker() { const [value, setValue] = React.useState>([null, null]); return ( - + setValue(newValue)} + onChange={(newValue) => { + setValue(newValue); + }} renderInput={(startProps, endProps) => ( @@ -27,7 +30,9 @@ export default function ResponsiveDateRangePicker() { setValue(newValue)} + onChange={(newValue) => { + setValue(newValue); + }} renderInput={(startProps, endProps) => ( @@ -36,6 +41,6 @@ export default function ResponsiveDateRangePicker() { )} /> - + ); } diff --git a/docs/src/pages/components/date-range-picker/StaticDateRangePicker.js b/docs/src/pages/components/date-range-picker/StaticDateRangePicker.js new file mode 100644 index 00000000000000..2d6e76e6f78ecc --- /dev/null +++ b/docs/src/pages/components/date-range-picker/StaticDateRangePicker.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import StaticDateRangePicker from '@material-ui/lab/StaticDateRangePicker'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; + +export default function StaticDateRangePickerExample() { + const [value, setValue] = React.useState([null, null]); + + return ( + + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-range-picker/StaticDateRangePicker.tsx b/docs/src/pages/components/date-range-picker/StaticDateRangePicker.tsx new file mode 100644 index 00000000000000..c8173872d206a4 --- /dev/null +++ b/docs/src/pages/components/date-range-picker/StaticDateRangePicker.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import StaticDateRangePicker, { + DateRange, +} from '@material-ui/lab/StaticDateRangePicker'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateRangeDelimiter from '@material-ui/lab/DateRangeDelimiter'; + +export default function StaticDateRangePickerExample() { + const [value, setValue] = React.useState>([null, null]); + + return ( + + { + setValue(newValue); + }} + renderInput={(startProps, endProps) => ( + + + to + + + )} + /> + + ); +} diff --git a/docs/src/pages/components/date-range-picker/date-range-picker.md b/docs/src/pages/components/date-range-picker/date-range-picker.md new file mode 100644 index 00000000000000..340da42b935179 --- /dev/null +++ b/docs/src/pages/components/date-range-picker/date-range-picker.md @@ -0,0 +1,83 @@ +--- +title: React Date Range Picker component +components: DateRangePicker +githubLabel: 'component: DateRangePicker' +packageName: '@material-ui/lab' +materialDesign: https://material.io/components/date-pickers +--- + +# Date Range Picker [⚡️](https://material-ui.com/store/items/material-ui-x/) + +

Date pickers let the user select a range of dates.

+ +> ⚠️⚠️ The date range picker is unstable, and **not suitable** for use in production. ⚠️⚠️ +>

+> The date range picker will be made available in the coming months for production use as part of a paid extension (commercial license) to the community edition (MIT license) of Material-UI. +> This paid extension will include advanced components (rich data grid, date range picker, tree view drag & drop, etc.). [Early access](https://material-ui.com/store/items/material-ui-x/) starts at an affordable price. + +The date range pickers let the user select a range of dates. + +{{"component": "modules/components/ComponentLinkHeader.js"}} + +## Requirements + +This component relies on the date management library of your choice. It supports [date-fns](https://date-fns.org/), [luxon](https://moment.github.io/luxon/), [dayjs](https://github.com/iamkun/dayjs), [moment](https://momentjs.com/) and any other library via a public `dateAdapter` interface. + +Please install any of these libraries and set up the right date engine by wrapping your root (or the highest level you wish the pickers to be available) with `LocalizationProvider`: + +```jsx +// or @material-ui/lab/dateAdapter/{dayjs,luxon,moment} or any valid date-io adapter +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; + +function App() { + return ( + + ... + + ); +} +``` + +## Basic usage + +Note that you can pass almost any prop from [DatePicker]('/api/date-picker/'). + +{{"demo": "pages/components/date-range-picker/BasicDateRangePicker.js"}} + +## Responsiveness + +The date range picker component is designed to be optimized for the device it runs on. + +- The "Mobile" version works best for touch devices and small screens. +- The "Desktop" version works best for mouse devices and large screens. + +By default, the `DateRangePicker` component uses a `@media (pointer: fine)` media query to determine which version to use. +This can be customized with the `desktopModeMediaQuery` prop. + +{{"demo": "pages/components/date-range-picker/ResponsiveDateRangePicker.js"}} + +## Different number of months + +Note that the `calendars` prop only works in desktop mode. + +{{"demo": "pages/components/date-range-picker/CalendarsDateRangePicker.js"}} + +## Disabling dates + +Disabling dates behaves the same as the simple `DatePicker`. + +{{"demo": "pages/components/date-range-picker/MinMaxDateRangePicker.js"}} + +## Custom input component + +You can customize the rendered input with the `renderInput` prop. For `DateRangePicker` it takes **2** parameters – for start and end input respectively. +If you need to render custom inputs make sure to spread `ref` and `inputProps` correctly to the input components. + +{{"demo": "pages/components/date-range-picker/CustomDateRangeInputs.js"}} + +## Static mode + +It is possible to render any picker without a modal or popper. For this use `StaticDateRangePicker`. + +{{"demo": "pages/components/date-range-picker/StaticDateRangePicker.js"}} diff --git a/docs/src/pages/components/date-time-picker/BasicDateTimePicker.js b/docs/src/pages/components/date-time-picker/BasicDateTimePicker.js new file mode 100644 index 00000000000000..b9c629297a01c8 --- /dev/null +++ b/docs/src/pages/components/date-time-picker/BasicDateTimePicker.js @@ -0,0 +1,20 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateTimePicker from '@material-ui/lab/DateTimePicker'; + +export default function BasicDateTimePicker() { + const [selectedDate, handleDateChange] = React.useState(new Date()); + + return ( + + } + label="DateTimePicker" + value={selectedDate} + onChange={handleDateChange} + /> + + ); +} diff --git a/packages/pickers/docs/pages/demo/datetime-picker/BasicDateTimePicker.example.tsx b/docs/src/pages/components/date-time-picker/BasicDateTimePicker.tsx similarity index 57% rename from packages/pickers/docs/pages/demo/datetime-picker/BasicDateTimePicker.example.tsx rename to docs/src/pages/components/date-time-picker/BasicDateTimePicker.tsx index c09161509794da..870355e74b7576 100644 --- a/packages/pickers/docs/pages/demo/datetime-picker/BasicDateTimePicker.example.tsx +++ b/docs/src/pages/components/date-time-picker/BasicDateTimePicker.tsx @@ -1,18 +1,22 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import { DateTimePicker } from '@material-ui/pickers'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateTimePicker from '@material-ui/lab/DateTimePicker'; export default function BasicDateTimePicker() { - const [selectedDate, handleDateChange] = React.useState(new Date()); + const [selectedDate, handleDateChange] = React.useState( + new Date(), + ); return ( - + } label="DateTimePicker" value={selectedDate} onChange={handleDateChange} /> - + ); } diff --git a/docs/src/pages/components/date-time-picker/CustomDateTimePicker.js b/docs/src/pages/components/date-time-picker/CustomDateTimePicker.js new file mode 100644 index 00000000000000..117b4f9acb394f --- /dev/null +++ b/docs/src/pages/components/date-time-picker/CustomDateTimePicker.js @@ -0,0 +1,74 @@ +import * as React from 'react'; +import AlarmIcon from '@material-ui/icons/Alarm'; +import SnoozeIcon from '@material-ui/icons/Snooze'; +import TextField from '@material-ui/core/TextField'; +import ClockIcon from '@material-ui/icons/AccessTime'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateTimePicker from '@material-ui/lab/DateTimePicker'; +import MobileDateTimePicker from '@material-ui/lab/MobileDateTimePicker'; + +export default function CustomDateTimePicker() { + const [clearedDate, setClearedDate] = React.useState(null); + const [value, setValue] = React.useState(new Date('2019-01-01T18:54')); + + return ( + +
+ { + setValue(newValue); + }} + minDate={new Date('2018-01-01')} + leftArrowIcon={} + rightArrowIcon={} + leftArrowButtonText="Open previous month" + rightArrowButtonText="Open next month" + openPickerIcon={} + minTime={new Date(0, 0, 0, 9)} + maxTime={new Date(0, 0, 0, 20)} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + label="With error handler" + onError={console.log} + minDate={new Date('2018-01-01T00:00')} + inputFormat="yyyy/MM/dd hh:mm a" + mask="___/__/__ __:__ _M" + renderInput={(params) => ( + + )} + /> + setClearedDate(newValue)} + renderInput={(params) => ( + + )} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-time-picker/CustomDateTimePicker.tsx b/docs/src/pages/components/date-time-picker/CustomDateTimePicker.tsx new file mode 100644 index 00000000000000..1e62a25889135d --- /dev/null +++ b/docs/src/pages/components/date-time-picker/CustomDateTimePicker.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import AlarmIcon from '@material-ui/icons/Alarm'; +import SnoozeIcon from '@material-ui/icons/Snooze'; +import TextField from '@material-ui/core/TextField'; +import ClockIcon from '@material-ui/icons/AccessTime'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateTimePicker from '@material-ui/lab/DateTimePicker'; +import MobileDateTimePicker from '@material-ui/lab/MobileDateTimePicker'; + +export default function CustomDateTimePicker() { + const [clearedDate, setClearedDate] = React.useState(null); + const [value, setValue] = React.useState( + new Date('2019-01-01T18:54'), + ); + + return ( + +
+ { + setValue(newValue); + }} + minDate={new Date('2018-01-01')} + leftArrowIcon={} + rightArrowIcon={} + leftArrowButtonText="Open previous month" + rightArrowButtonText="Open next month" + openPickerIcon={} + minTime={new Date(0, 0, 0, 9)} + maxTime={new Date(0, 0, 0, 20)} + renderInput={(params) => ( + + )} + /> + { + setValue(newValue); + }} + label="With error handler" + onError={console.log} + minDate={new Date('2018-01-01T00:00')} + inputFormat="yyyy/MM/dd hh:mm a" + mask="___/__/__ __:__ _M" + renderInput={(params) => ( + + )} + /> + setClearedDate(newValue)} + renderInput={(params) => ( + + )} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-time-picker/DateTimeValidation.js b/docs/src/pages/components/date-time-picker/DateTimeValidation.js new file mode 100644 index 00000000000000..0f8fd008adfbf9 --- /dev/null +++ b/docs/src/pages/components/date-time-picker/DateTimeValidation.js @@ -0,0 +1,36 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateTimePicker from '@material-ui/lab/DateTimePicker'; + +export default function DateTimeValidation() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ } + label="Ignore date and time" + value={value} + onChange={(newValue) => { + setValue(newValue); + }} + minDateTime={new Date()} + /> + } + label="Ignore time in each day" + value={value} + onChange={(newValue) => { + setValue(newValue); + }} + minDate={new Date('2020-02-14')} + minTime={new Date(0, 0, 0, 8)} + maxTime={new Date(0, 0, 0, 18, 45)} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-time-picker/DateTimeValidation.tsx b/docs/src/pages/components/date-time-picker/DateTimeValidation.tsx new file mode 100644 index 00000000000000..18936a6cd6d60c --- /dev/null +++ b/docs/src/pages/components/date-time-picker/DateTimeValidation.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateTimePicker from '@material-ui/lab/DateTimePicker'; + +export default function DateTimeValidation() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ } + label="Ignore date and time" + value={value} + onChange={(newValue) => { + setValue(newValue); + }} + minDateTime={new Date()} + /> + } + label="Ignore time in each day" + value={value} + onChange={(newValue) => { + setValue(newValue); + }} + minDate={new Date('2020-02-14')} + minTime={new Date(0, 0, 0, 8)} + maxTime={new Date(0, 0, 0, 18, 45)} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-time-picker/ResponsiveDateTimePickers.js b/docs/src/pages/components/date-time-picker/ResponsiveDateTimePickers.js new file mode 100644 index 00000000000000..1e0c9d1f511161 --- /dev/null +++ b/docs/src/pages/components/date-time-picker/ResponsiveDateTimePickers.js @@ -0,0 +1,41 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateTimePicker from '@material-ui/lab/DateTimePicker'; +import MobileDateTimePicker from '@material-ui/lab/MobileDateTimePicker'; +import DesktopDateTimePicker from '@material-ui/lab/DesktopDateTimePicker'; + +export default function ResponsiveDateTimePickers() { + const [value, setValue] = React.useState( + new Date('2018-01-01T00:00:00.000Z'), + ); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> + } + value={value} + onChange={(newValue) => { + setValue(newValue); + }} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-time-picker/ResponsiveDateTimePickers.tsx b/docs/src/pages/components/date-time-picker/ResponsiveDateTimePickers.tsx new file mode 100644 index 00000000000000..b730f644cba691 --- /dev/null +++ b/docs/src/pages/components/date-time-picker/ResponsiveDateTimePickers.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateTimePicker from '@material-ui/lab/DateTimePicker'; +import MobileDateTimePicker from '@material-ui/lab/MobileDateTimePicker'; +import DesktopDateTimePicker from '@material-ui/lab/DesktopDateTimePicker'; + +export default function ResponsiveDateTimePickers() { + const [value, setValue] = React.useState( + new Date('2018-01-01T00:00:00.000Z'), + ); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> + } + value={value} + onChange={(newValue) => { + setValue(newValue); + }} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/date-time-picker/date-time-picker.md b/docs/src/pages/components/date-time-picker/date-time-picker.md new file mode 100644 index 00000000000000..d192666a7c59d3 --- /dev/null +++ b/docs/src/pages/components/date-time-picker/date-time-picker.md @@ -0,0 +1,71 @@ +--- +title: React Date Time Picker component +components: DateTimePicker +githubLabel: 'component: DateTimePicker' +packageName: '@material-ui/lab' +materialDesign: https://material.io/components/date-pickers +--- + +# Date Time Picker + +

Combined date & time picker.

+ +This component combines the date & time pickers. It allows the user to select both date and time with the same control. + +Note that this component is the [DatePicker](/components/date-picker/) and [TimePicker](/components/time-picker/) +component combined, so any of these components' props can be passed to the DateTimePicker. + +{{"component": "modules/components/ComponentLinkHeader.js"}} + +## Requirements + +This component relies on the date management library of your choice. It supports [date-fns](https://date-fns.org/), [luxon](https://moment.github.io/luxon/), [dayjs](https://github.com/iamkun/dayjs), [moment](https://momentjs.com/) and any other library via a public `dateAdapter` interface. + +Please install any of these libraries and set up the right date engine by wrapping your root (or the highest level you wish the pickers to be available) with `LocalizationProvider`: + +```jsx +// or @material-ui/lab/dateAdapter/{dayjs,luxon,moment} or any valid date-io adapter +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; + +function App() { + return ( + + ... + + ); +} +``` + +## Basic usage + +Allows choosing date then time. There are 4 steps available (year, date, hour and minute), so tabs are required to visually distinguish date/time steps. + +{{"demo": "pages/components/date-time-picker/BasicDateTimePicker.js"}} + +## Responsiveness + +The `DateTimePicker` component is designed and optimized for the device it runs on. + +- The "Mobile" version works best for touch devices and small screens. +- The "Desktop" version works best for mouse devices and large screens. + +By default, the `DateTimePicker` component uses a `@media (pointer: fine)` media query to determine which version to use. +This can be customized with the `desktopModeMediaQuery` prop. + +{{"demo": "pages/components/date-time-picker/ResponsiveDateTimePickers.js"}} + +## Date and time validation + +It is possible to restrict date and time selection in two ways: + +- by using `minDateTime`/`maxDateTime` its possible to restrict time selection to before or after a particular moment in time +- using `minTime`/`maxTime`, you can disable selecting times before or after a certain time each day respectively + +{{"demo": "pages/components/date-time-picker/DateTimeValidation.js"}} + +## Customization + +Here are some examples of heavily customized date & time pickers: + +{{"demo": "pages/components/date-time-picker/CustomDateTimePicker.js"}} diff --git a/docs/src/pages/components/pickers/MaterialUIPickers.js b/docs/src/pages/components/pickers/MaterialUIPickers.js index 9a5c4a9898f5cb..065bad26371316 100644 --- a/docs/src/pages/components/pickers/MaterialUIPickers.js +++ b/docs/src/pages/components/pickers/MaterialUIPickers.js @@ -1,16 +1,13 @@ import * as React from 'react'; import Grid from '@material-ui/core/Grid'; import TextField from '@material-ui/core/TextField'; -import DateFnsAdapter from '@material-ui/pickers/adapter/date-fns'; -import { - LocalizationProvider as MuiPickersLocalizationProvider, - TimePicker, - DesktopDatePicker, - MobileDatePicker, -} from '@material-ui/pickers'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; +import DesktopDatePicker from '@material-ui/lab/DesktopDatePicker'; +import MobileDatePicker from '@material-ui/lab/MobileDatePicker'; export default function MaterialUIPickers() { - // The first commit of Material-UI const [selectedDate, setSelectedDate] = React.useState( new Date('2014-08-18T21:11:54'), ); @@ -20,15 +17,15 @@ export default function MaterialUIPickers() { }; return ( - + ( - + renderInput={(params) => ( + )} OpenPickerButtonProps={{ 'aria-label': 'change date', @@ -39,8 +36,8 @@ export default function MaterialUIPickers() { inputFormat="MM/dd/yyyy" value={selectedDate} onChange={handleDateChange} - renderInput={(props) => ( - + renderInput={(params) => ( + )} OpenPickerButtonProps={{ 'aria-label': 'change date', @@ -50,12 +47,12 @@ export default function MaterialUIPickers() { label="Time picker" value={selectedDate} onChange={handleDateChange} - renderInput={(props) => } + renderInput={(params) => } OpenPickerButtonProps={{ 'aria-label': 'change time', }} /> - + ); } diff --git a/docs/src/pages/components/pickers/MaterialUIPickers.tsx b/docs/src/pages/components/pickers/MaterialUIPickers.tsx index 7dbf199d57a204..512877d487b144 100644 --- a/docs/src/pages/components/pickers/MaterialUIPickers.tsx +++ b/docs/src/pages/components/pickers/MaterialUIPickers.tsx @@ -1,16 +1,13 @@ import * as React from 'react'; import Grid from '@material-ui/core/Grid'; import TextField from '@material-ui/core/TextField'; -import DateFnsAdapter from '@material-ui/pickers/adapter/date-fns'; -import { - LocalizationProvider as MuiPickersLocalizationProvider, - TimePicker, - DesktopDatePicker, - MobileDatePicker, -} from '@material-ui/pickers'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; +import DesktopDatePicker from '@material-ui/lab/DesktopDatePicker'; +import MobileDatePicker from '@material-ui/lab/MobileDatePicker'; export default function MaterialUIPickers() { - // The first commit of Material-UI const [selectedDate, setSelectedDate] = React.useState( new Date('2014-08-18T21:11:54'), ); @@ -20,15 +17,15 @@ export default function MaterialUIPickers() { }; return ( - + ( - + renderInput={(params) => ( + )} OpenPickerButtonProps={{ 'aria-label': 'change date', @@ -39,8 +36,8 @@ export default function MaterialUIPickers() { inputFormat="MM/dd/yyyy" value={selectedDate} onChange={handleDateChange} - renderInput={(props) => ( - + renderInput={(params) => ( + )} OpenPickerButtonProps={{ 'aria-label': 'change date', @@ -50,12 +47,12 @@ export default function MaterialUIPickers() { label="Time picker" value={selectedDate} onChange={handleDateChange} - renderInput={(props) => } + renderInput={(params) => } OpenPickerButtonProps={{ 'aria-label': 'change time', }} /> - + ); } diff --git a/docs/src/pages/components/pickers/pickers.md b/docs/src/pages/components/pickers/pickers.md index cfec750aee9fd4..af4aa0c17a8e5b 100644 --- a/docs/src/pages/components/pickers/pickers.md +++ b/docs/src/pages/components/pickers/pickers.md @@ -16,19 +16,13 @@ packageName: '@material-ui/lab' {{"component": "modules/components/ComponentLinkHeader.js"}} -## @material-ui/pickers - -![stars](https://img.shields.io/github/stars/mui-org/material-ui-pickers.svg?style=social&label=Stars) -![npm downloads](https://img.shields.io/npm/dm/@material-ui/pickers.svg) - -[@material-ui/pickers](https://material-ui-pickers.dev/) provides date picker and time picker controls. +## React components {{"demo": "pages/components/pickers/MaterialUIPickers.js"}} ## Native pickers ⚠️ Native input controls support by browsers [isn't perfect](https://caniuse.com/#feat=input-datetime). -Have a look at [@material-ui/pickers](https://material-ui-pickers.dev/) for a richer solution. ### Datepickers diff --git a/docs/src/pages/components/progress/DelayingAppearance.js b/docs/src/pages/components/progress/DelayingAppearance.js index ff03dd29a768ea..826a99045294dc 100644 --- a/docs/src/pages/components/progress/DelayingAppearance.js +++ b/docs/src/pages/components/progress/DelayingAppearance.js @@ -37,7 +37,9 @@ export default function DelayingAppearance() { }; const handleClickQuery = () => { - clearTimeout(timerRef.current); + if (timerRef.current) { + clearTimeout(timerRef.current); + } if (query !== 'idle') { setQuery('idle'); diff --git a/docs/src/pages/components/progress/DelayingAppearance.tsx b/docs/src/pages/components/progress/DelayingAppearance.tsx index 03e15e1899df59..5ba7af3f0a5c9e 100644 --- a/docs/src/pages/components/progress/DelayingAppearance.tsx +++ b/docs/src/pages/components/progress/DelayingAppearance.tsx @@ -39,7 +39,9 @@ export default function DelayingAppearance() { }; const handleClickQuery = () => { - clearTimeout(timerRef.current); + if (timerRef.current) { + clearTimeout(timerRef.current); + } if (query !== 'idle') { setQuery('idle'); diff --git a/docs/src/pages/components/time-picker/BasicTimePicker.js b/docs/src/pages/components/time-picker/BasicTimePicker.js new file mode 100644 index 00000000000000..e8706e1df15c00 --- /dev/null +++ b/docs/src/pages/components/time-picker/BasicTimePicker.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; + +export default function BasicTimePicker() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/BasicTimePicker.tsx b/docs/src/pages/components/time-picker/BasicTimePicker.tsx new file mode 100644 index 00000000000000..9298df571b262c --- /dev/null +++ b/docs/src/pages/components/time-picker/BasicTimePicker.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; + +export default function BasicTimePicker() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/LocalizedTimePicker.js b/docs/src/pages/components/time-picker/LocalizedTimePicker.js new file mode 100644 index 00000000000000..cf38f994d9b9e2 --- /dev/null +++ b/docs/src/pages/components/time-picker/LocalizedTimePicker.js @@ -0,0 +1,56 @@ +import * as React from 'react'; +import frLocale from 'date-fns/locale/fr'; +import ruLocale from 'date-fns/locale/ru'; +import arSaLocale from 'date-fns/locale/ar-SA'; +import enLocale from 'date-fns/locale/en-US'; +import ToggleButton from '@material-ui/core/ToggleButton'; +import ToggleButtonGroup from '@material-ui/core/ToggleButtonGroup'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; + +import TimePicker from '@material-ui/lab/TimePicker'; + +const localeMap = { + en: enLocale, + fr: frLocale, + ru: ruLocale, + ar: arSaLocale, +}; + +export default function LocalizedTimePicker() { + const [locale, setLocale] = React.useState('ru'); + const [selectedDate, handleDateChange] = React.useState(new Date()); + + const selectLocale = (newLocale) => { + setLocale(newLocale); + }; + + return ( + +
+ + handleDateChange(date)} + renderInput={(params) => } + /> + + {Object.keys(localeMap).map((localeItem) => ( + selectLocale(localeItem)} + > + {localeItem} + + ))} + + +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/LocalizedTimePicker.tsx b/docs/src/pages/components/time-picker/LocalizedTimePicker.tsx new file mode 100644 index 00000000000000..74c74c4604b347 --- /dev/null +++ b/docs/src/pages/components/time-picker/LocalizedTimePicker.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import frLocale from 'date-fns/locale/fr'; +import ruLocale from 'date-fns/locale/ru'; +import arSaLocale from 'date-fns/locale/ar-SA'; +import enLocale from 'date-fns/locale/en-US'; +import ToggleButton from '@material-ui/core/ToggleButton'; +import ToggleButtonGroup from '@material-ui/core/ToggleButtonGroup'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; + +import TimePicker from '@material-ui/lab/TimePicker'; + +const localeMap = { + en: enLocale, + fr: frLocale, + ru: ruLocale, + ar: arSaLocale, +}; + +export default function LocalizedTimePicker() { + const [locale, setLocale] = React.useState('ru'); + const [selectedDate, handleDateChange] = React.useState( + new Date(), + ); + + const selectLocale = (newLocale: any) => { + setLocale(newLocale); + }; + + return ( + +
+ + handleDateChange(date)} + renderInput={(params) => } + /> + + {Object.keys(localeMap).map((localeItem) => ( + selectLocale(localeItem)} + > + {localeItem} + + ))} + + +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/ResponsiveTimePickers.js b/docs/src/pages/components/time-picker/ResponsiveTimePickers.js new file mode 100644 index 00000000000000..dc77f244a5760c --- /dev/null +++ b/docs/src/pages/components/time-picker/ResponsiveTimePickers.js @@ -0,0 +1,41 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; +import MobileTimePicker from '@material-ui/lab/MobileTimePicker'; +import DesktopTimePicker from '@material-ui/lab/DesktopTimePicker'; + +export default function ResponsiveTimePickers() { + const [value, setValue] = React.useState( + new Date('2018-01-01T00:00:00.000Z'), + ); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> + } + /> +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/ResponsiveTimePickers.tsx b/docs/src/pages/components/time-picker/ResponsiveTimePickers.tsx new file mode 100644 index 00000000000000..0552187c375a61 --- /dev/null +++ b/docs/src/pages/components/time-picker/ResponsiveTimePickers.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; +import MobileTimePicker from '@material-ui/lab/MobileTimePicker'; +import DesktopTimePicker from '@material-ui/lab/DesktopTimePicker'; + +export default function ResponsiveTimePickers() { + const [value, setValue] = React.useState( + new Date('2018-01-01T00:00:00.000Z'), + ); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> + } + /> +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/SecondsTimePicker.js b/docs/src/pages/components/time-picker/SecondsTimePicker.js new file mode 100644 index 00000000000000..91a5752892c293 --- /dev/null +++ b/docs/src/pages/components/time-picker/SecondsTimePicker.js @@ -0,0 +1,41 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; + +export default function SecondsTimePicker() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/SecondsTimePicker.tsx b/docs/src/pages/components/time-picker/SecondsTimePicker.tsx new file mode 100644 index 00000000000000..c64959595806c8 --- /dev/null +++ b/docs/src/pages/components/time-picker/SecondsTimePicker.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; + +export default function SecondsTimePicker() { + const [value, setValue] = React.useState(new Date()); + + return ( + +
+ { + setValue(newValue); + }} + renderInput={(params) => } + /> + { + setValue(newValue); + }} + renderInput={(params) => } + /> +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/StaticTimePickerDemo.js b/docs/src/pages/components/time-picker/StaticTimePickerDemo.js new file mode 100644 index 00000000000000..61a317584eadc3 --- /dev/null +++ b/docs/src/pages/components/time-picker/StaticTimePickerDemo.js @@ -0,0 +1,22 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import StaticTimePicker from '@material-ui/lab/StaticTimePicker'; + +export default function StaticTimePickerDemo() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/time-picker/StaticTimePickerDemo.tsx b/docs/src/pages/components/time-picker/StaticTimePickerDemo.tsx new file mode 100644 index 00000000000000..e0289e25b26f98 --- /dev/null +++ b/docs/src/pages/components/time-picker/StaticTimePickerDemo.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import StaticTimePicker from '@material-ui/lab/StaticTimePicker'; + +export default function StaticTimePickerDemo() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/time-picker/StaticTimePickerLandscape.js b/docs/src/pages/components/time-picker/StaticTimePickerLandscape.js new file mode 100644 index 00000000000000..1109b159aa3df6 --- /dev/null +++ b/docs/src/pages/components/time-picker/StaticTimePickerLandscape.js @@ -0,0 +1,24 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import StaticTimePicker from '@material-ui/lab/StaticTimePicker'; + +export default function StaticTimePickerLandscape() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/time-picker/StaticTimePickerLandscape.tsx b/docs/src/pages/components/time-picker/StaticTimePickerLandscape.tsx new file mode 100644 index 00000000000000..31d4043833e313 --- /dev/null +++ b/docs/src/pages/components/time-picker/StaticTimePickerLandscape.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import StaticTimePicker from '@material-ui/lab/StaticTimePicker'; + +export default function StaticTimePickerLandscape() { + const [value, setValue] = React.useState(new Date()); + + return ( + + { + setValue(newValue); + }} + renderInput={(params) => } + /> + + ); +} diff --git a/docs/src/pages/components/time-picker/TimeValidationTimePicker.js b/docs/src/pages/components/time-picker/TimeValidationTimePicker.js new file mode 100644 index 00000000000000..fc35465cd28105 --- /dev/null +++ b/docs/src/pages/components/time-picker/TimeValidationTimePicker.js @@ -0,0 +1,41 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; + +export default function TimeValidationTimePicker() { + const [value, setValue] = React.useState(new Date('2020-01-01 12:00')); + + return ( + +
+ } + value={value} + label="min/max time" + onChange={(newValue) => { + setValue(newValue); + }} + minTime={new Date(0, 0, 0, 8)} + maxTime={new Date(0, 0, 0, 18, 45)} + /> + } + label="Disable odd hours" + value={value} + onChange={(newValue) => { + setValue(newValue); + }} + shouldDisableTime={(timeValue, clockType) => { + if (clockType === 'hours' && timeValue % 2) { + return true; + } + + return false; + }} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/TimeValidationTimePicker.tsx b/docs/src/pages/components/time-picker/TimeValidationTimePicker.tsx new file mode 100644 index 00000000000000..c096073a6af2ed --- /dev/null +++ b/docs/src/pages/components/time-picker/TimeValidationTimePicker.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import TimePicker from '@material-ui/lab/TimePicker'; + +export default function TimeValidationTimePicker() { + const [value, setValue] = React.useState( + new Date('2020-01-01 12:00'), + ); + + return ( + +
+ } + value={value} + label="min/max time" + onChange={(newValue) => { + setValue(newValue); + }} + minTime={new Date(0, 0, 0, 8)} + maxTime={new Date(0, 0, 0, 18, 45)} + /> + } + label="Disable odd hours" + value={value} + onChange={(newValue) => { + setValue(newValue); + }} + shouldDisableTime={(timeValue, clockType) => { + if (clockType === 'hours' && timeValue % 2) { + return true; + } + + return false; + }} + /> +
+
+ ); +} diff --git a/docs/src/pages/components/time-picker/time-picker.md b/docs/src/pages/components/time-picker/time-picker.md new file mode 100644 index 00000000000000..bea749062b1f65 --- /dev/null +++ b/docs/src/pages/components/time-picker/time-picker.md @@ -0,0 +1,80 @@ +--- +title: React Time Picker component +components: TimePicker +githubLabel: 'component: TimePicker' +packageName: '@material-ui/lab' +materialDesign: https://material.io/components/time-pickers +--- + +# Time Picker + +

Time pickers allow the user to select a single time.

+ +Time pickers allow the user to select a single time (in the hours:minutes format). +The selected time is indicated by the filled circle at the end of the clock hand. + +{{"component": "modules/components/ComponentLinkHeader.js"}} + +## Requirements + +This component relies on the date management library of your choice. It supports [date-fns](https://date-fns.org/), [luxon](https://moment.github.io/luxon/), [dayjs](https://github.com/iamkun/dayjs), [moment](https://momentjs.com/) and any other library via a public `dateAdapter` interface. + +Please install any of these libraries and set up the right date engine by wrapping your root (or the highest level you wish the pickers to be available) with `LocalizationProvider`: + +```jsx +// or @material-ui/lab/dateAdapter/{dayjs,luxon,moment} or any valid date-io adapter +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; + +function App() { + return ( + + ... + + ); +} +``` + +## Basic usage + +The time picker will automatically adjust to the locale's time setting, i.e. the 12-hour or 24-hour format. This can be controlled with `ampm` prop. + +{{"demo": "pages/components/time-picker/BasicTimePicker.js"}} + +## Localization + +Use `LocalizationProvider` to change the date-engine locale that is used to render the time picker. Note that `am/pm` setting is switched automatically: + +{{"demo": "pages/components/time-picker/LocalizedTimePicker.js"}} + +## Responsiveness + +The time picker component is designed and optimized for the device it runs on. + +- The "Mobile" version works best for touch devices and small screens. +- The "Desktop" version works best for mouse devices and large screens. + +By default, the `TimePicker` component uses a `@media (pointer: fine)` media query to determine which version to use. +This can be customized with the `desktopModeMediaQuery` prop. + +{{"demo": "pages/components/time-picker/ResponsiveTimePickers.js"}} + +## Time validation + +{{"demo": "pages/components/time-picker/TimeValidationTimePicker.js"}} + +## Static mode + +It's possible to render any picker inline. This will enable building custom popover/modal containers. + +{{"demo": "pages/components/time-picker/StaticTimePickerDemo.js", "bg": true}} + +## Landscape + +{{"demo": "pages/components/time-picker/StaticTimePickerLandscape.js", "bg": true}} + +## Seconds + +The seconds input can be used for selection of a precise time point. + +{{"demo": "pages/components/time-picker/SecondsTimePicker.js"}} diff --git a/docs/translations/translations.json b/docs/translations/translations.json index 2e9b713d681202..ea991e666e3ee4 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -134,7 +134,6 @@ "/components/button-group": "Button Group", "/components/checkboxes": "Checkbox", "/components/floating-action-button": "Floating Action Button", - "/components/pickers": "Date / Time", "/components/radio-buttons": "Radio button", "/components/rating": "Rating", "/components/selects": "Select", @@ -202,6 +201,12 @@ "/components/use-media-query": "useMediaQuery", "/components/lab": "Lab", "/components/about-the-lab": "About the lab 🧪", + "/components/lab-pickers": "Date / Time", + "/components/pickers": "Introduction", + "/components/date-picker": "Date Picker", + "/components/date-range-picker": "Date Range Picker", + "/components/date-time-picker": "Date Time Picker", + "/components/time-picker": "Time Picker", "/components/slider-styled": "Slider Styled", "/components/timeline": "Timeline", "/components/trap-focus": "Trap Focus", diff --git a/package.json b/package.json index b0a6f7fba43bdd..ae5142d8dd1a7f 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,13 @@ "docs:mdicons:synonyms": "babel-node --config-file ./babel.config.js ./docs/scripts/updateIconSynonyms", "extract-error-codes": "lerna run --parallel extract-error-codes", "framer:build": "yarn workspace framer build", - "jsonlint": "node scripts/jsonlint.js", + "jsonlint": "node ./scripts/jsonlint.js", "lint": "eslint . --cache --report-unused-disable-directives --ext .js,.ts,.tsx", "lint:ci": "eslint . --report-unused-disable-directives --ext .js,.ts,.tsx", "stylelint": "stylelint 'docs/**/*.js' 'docs/**/*.ts' 'docs/**/*.tsx'", "prettier": "node ./scripts/prettier.js", "prettier:all": "node ./scripts/prettier.js write", - "size:snapshot": "node scripts/sizeSnapshot/create", + "size:snapshot": "node --max-old-space-size=2048 ./scripts/sizeSnapshot/create", "size:why": "node scripts/sizeSnapshot/why", "start": "yarn && yarn docs:dev", "t": "node test/cli.js", @@ -181,6 +181,7 @@ "nyc": { "include": [ "packages/material-ui/src/**/*.js", + "packages/material-ui/lab/**/*.{ts,tsx}", "packages/material-ui-utils/src/**/*.js", "packages/material-ui-styles/src/**/*.js" ], diff --git a/packages/eslint-plugin-material-ui/README.md b/packages/eslint-plugin-material-ui/README.md index a9f2ed702e6460..b5e5fd45548798 100644 --- a/packages/eslint-plugin-material-ui/README.md +++ b/packages/eslint-plugin-material-ui/README.md @@ -7,6 +7,7 @@ Custom eslint rules for Material-UI. - `disallow-active-element-as-key-event-target` - `docgen-ignore-before-comment` - `no-hardcoded-labels` +- `lower-case-test-name` - ~~`restricted-path-imports`~~ ### disallow-active-element-as-key-event-target diff --git a/packages/material-ui-codemod/src/v1.0.0/import-path.test/actual.js b/packages/material-ui-codemod/src/v1.0.0/import-path.test/actual.js index 4dc0b3f4bf1705..d7ad017d5e7ae8 100644 --- a/packages/material-ui-codemod/src/v1.0.0/import-path.test/actual.js +++ b/packages/material-ui-codemod/src/v1.0.0/import-path.test/actual.js @@ -14,11 +14,7 @@ import List, { ListItemSecondaryAction, } from '@material-ui/core/List'; import Dialog, { DialogTitle } from '@material-ui/core/Dialog'; -import { - DialogActions, - DialogContent, - DialogContentText, -} from '@material-ui/core/Dialog'; +import { DialogActions, DialogContent, DialogContentText } from '@material-ui/core/Dialog'; import Slide from '@material-ui/core/transitions/Slide'; import Radio, { RadioGroup } from '@material-ui/core/Radio'; import { FormControlLabel } from '@material-ui/core/Form'; diff --git a/packages/material-ui-lab/package.json b/packages/material-ui-lab/package.json index debdb2e41bf2ae..73cb6d49fff793 100644 --- a/packages/material-ui-lab/package.json +++ b/packages/material-ui-lab/package.json @@ -38,12 +38,28 @@ "peerDependencies": { "@material-ui/core": "^5.0.0-alpha.11", "@types/react": "^16.8.6 || ^17.0.0", + "date-fns": "^2.0.0", + "dayjs": "^1.8.17", + "luxon": "^1.21.3", + "moment": "^2.24.0", "react": "^16.8.0 || ^17.0.0", "react-dom": "^16.8.0 || ^17.0.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true } }, "dependencies": { @@ -53,10 +69,21 @@ "@material-ui/utils": "^5.0.0-alpha.15", "clsx": "^1.0.4", "prop-types": "^15.7.2", - "react-is": "^16.8.0 || ^17.0.0" + "react-is": "^16.8.0 || ^17.0.0", + "@date-io/date-fns": "^2.8.0", + "@date-io/dayjs": "^2.8.0", + "@date-io/luxon": "^2.8.0", + "@date-io/moment": "^2.8.0", + "react-transition-group": "^4.4.1", + "rifm": "^0.12.0" }, "devDependencies": { - "@material-ui/types": "^5.1.0" + "@material-ui/types": "^5.1.0", + "@types/luxon": "^0.5.2", + "date-fns": "^2.0.0", + "dayjs": "^1.8.17", + "luxon": "^1.21.3", + "moment": "^2.24.0" }, "sideEffects": false, "publishConfig": { diff --git a/packages/pickers/lib/src/views/Clock/Clock.tsx b/packages/material-ui-lab/src/ClockPicker/Clock.tsx similarity index 73% rename from packages/pickers/lib/src/views/Clock/Clock.tsx rename to packages/material-ui-lab/src/ClockPicker/Clock.tsx index a6c2d07d76f409..a7a5f783dbf6fb 100644 --- a/packages/pickers/lib/src/views/Clock/Clock.tsx +++ b/packages/material-ui-lab/src/ClockPicker/Clock.tsx @@ -1,41 +1,42 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import clsx from 'clsx'; import IconButton from '@material-ui/core/IconButton'; import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import { createStyles, WithStyles, Theme, withStyles } from '@material-ui/core/styles'; import ClockPointer from './ClockPointer'; -import { useUtils } from '../../_shared/hooks/useUtils'; -import { VIEW_HEIGHT } from '../../constants/dimensions'; -import { ClockViewType } from '../../constants/ClockType'; -import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -import { getHours, getMinutes } from '../../_helpers/time-utils'; -import { useDefaultProps } from '../../_shared/withDefaultProps'; -import { useMeridiemMode } from '../../TimePicker/TimePickerToolbar'; -import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; -import { useGlobalKeyDown, keycode } from '../../_shared/hooks/useKeyDown'; -import { WrapperVariantContext } from '../../wrappers/WrapperVariantContext'; +import { useUtils, MuiPickersAdapter } from '../internal/pickers/hooks/useUtils'; +import { VIEW_HEIGHT } from '../internal/pickers/constants/dimensions'; +import { ClockViewType } from '../internal/pickers/constants/ClockType'; +import { getHours, getMinutes } from '../internal/pickers/time-utils'; +import { useGlobalKeyDown, keycode } from '../internal/pickers/hooks/useKeyDown'; +import { + WrapperVariantContext, + IsStaticVariantContext, +} from '../internal/pickers/wrappers/WrapperVariantContext'; +import { PickerSelectionState } from '../internal/pickers/hooks/usePickerState'; +import { useMeridiemMode } from '../internal/pickers/hooks/date-helpers-hooks'; export interface ClockProps extends ReturnType { date: TDate | null; type: ClockViewType; value: number; isTimeDisabled: (timeValue: number, type: ClockViewType) => boolean; - children: React.ReactElement[]; - onDateChange: PickerOnChangeFn; + children: React.ReactNode[]; onChange: (value: number, isFinish?: PickerSelectionState) => void; ampm?: boolean; minutesStep?: number; ampmInClock?: boolean; allowKeyboardControl?: boolean; + getClockLabelText: ( + view: 'hours' | 'minutes' | 'seconds', + time: TDate, + adapter: MuiPickersAdapter, + ) => string; } -const muiComponentConfig = { - name: 'MuiPickersClock', -}; - -export const useStyles = makeStyles( - (theme) => ({ +export const styles = (theme: Theme) => + createStyles({ root: { display: 'flex', justifyContent: 'center', @@ -92,16 +93,20 @@ export const useStyles = makeStyles( backgroundColor: theme.palette.primary.light, }, }, - }), - muiComponentConfig -); + }); + +export type ClockClassKey = keyof WithStyles['classes']; -export function Clock(props: ClockProps) { +/** + * @ignore - internal component. + */ +function Clock(props: ClockProps & WithStyles) { const { allowKeyboardControl, ampm, ampmInClock = false, children: numbersElementsArray, + classes, date, handleMeridiemChange, isTimeDisabled, @@ -110,10 +115,11 @@ export function Clock(props: ClockProps) { onChange, type, value, - } = useDefaultProps(props, muiComponentConfig); + getClockLabelText, + } = props; const utils = useUtils(); - const classes = useStyles(); + const isStatic = React.useContext(IsStaticVariantContext); const wrapperVariant = React.useContext(WrapperVariantContext); const isMoving = React.useRef(false); @@ -138,12 +144,12 @@ export function Clock(props: ClockProps) { offsetY = e.changedTouches[0].clientY - rect.top; } - const value = + const newSelectedValue = type === 'seconds' || type === 'minutes' ? getMinutes(offsetX, offsetY, minutesStep) : getHours(offsetX, offsetY, Boolean(ampm)); - handleValueChange(value, isFinish); + handleValueChange(newSelectedValue, isFinish); }; const handleTouchMove = (e: React.TouchEvent) => { @@ -163,6 +169,7 @@ export function Clock(props: ClockProps) { e.stopPropagation(); // MouseEvent.which is deprecated, but MouseEvent.buttons is not supported in Safari const isButtonPressed = + // tslint:disable-next-line deprecation typeof e.buttons === 'undefined' ? e.nativeEvent.which === 1 : e.buttons === 1; if (isButtonPressed) { @@ -187,21 +194,19 @@ export function Clock(props: ClockProps) { }, [type, value]); const keyboardControlStep = type === 'minutes' ? minutesStep : 1; - useGlobalKeyDown( - Boolean(allowKeyboardControl ?? wrapperVariant !== 'static') && !isMoving.current, - { - [keycode.Home]: () => handleValueChange(0, 'partial'), // annulate both hours and minutes - [keycode.End]: () => handleValueChange(type === 'minutes' ? 59 : 23, 'partial'), - [keycode.ArrowUp]: () => handleValueChange(value + keyboardControlStep, 'partial'), - [keycode.ArrowDown]: () => handleValueChange(value - keyboardControlStep, 'partial'), - } - ); + useGlobalKeyDown(Boolean(allowKeyboardControl ?? !isStatic) && !isMoving.current, { + [keycode.Home]: () => handleValueChange(0, 'partial'), // annulate both hours and minutes + [keycode.End]: () => handleValueChange(type === 'minutes' ? 59 : 23, 'partial'), + [keycode.ArrowUp]: () => handleValueChange(value + keyboardControlStep, 'partial'), + [keycode.ArrowDown]: () => handleValueChange(value - keyboardControlStep, 'partial'), + }); return (
(props: ClockProps) { isInner={isPointerInner} hasSelected={hasSelected} aria-live="polite" - aria-label={`Selected time ${utils.format(date, 'fullTime')}`} + aria-label={getClockLabelText(type, date, utils)} /> )} @@ -259,4 +264,6 @@ Clock.propTypes = { minutesStep: PropTypes.number, } as any; -Clock.displayName = 'Clock'; +export default withStyles(styles, { + name: 'MuiClock', +})(Clock) as (props: ClockProps) => JSX.Element; diff --git a/packages/pickers/lib/src/views/Clock/ClockNumber.tsx b/packages/material-ui-lab/src/ClockPicker/ClockNumber.tsx similarity index 55% rename from packages/pickers/lib/src/views/Clock/ClockNumber.tsx rename to packages/material-ui-lab/src/ClockPicker/ClockNumber.tsx index aefb8c3e020ba7..8322211d94f7be 100644 --- a/packages/pickers/lib/src/views/Clock/ClockNumber.tsx +++ b/packages/material-ui-lab/src/ClockPicker/ClockNumber.tsx @@ -2,10 +2,10 @@ import * as React from 'react'; import clsx from 'clsx'; import Typography from '@material-ui/core/Typography'; import ButtonBase from '@material-ui/core/ButtonBase'; -import { makeStyles, fade } from '@material-ui/core/styles'; -import { onSpaceOrEnter } from '../../_helpers/utils'; -import { useCanAutoFocus } from '../../_shared/hooks/useCanAutoFocus'; -import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; +import { createStyles, WithStyles, withStyles, Theme, alpha } from '@material-ui/core/styles'; +import { onSpaceOrEnter } from '../internal/pickers/utils'; +import { useCanAutoFocus } from '../internal/pickers/hooks/useCanAutoFocus'; +import { PickerSelectionState } from '../internal/pickers/hooks/usePickerState'; const positions: Record = { 0: [0, 40], @@ -44,44 +44,55 @@ export interface ClockNumberProps { selected: boolean; } -export const useStyles = makeStyles( - (theme) => { - const size = 32; - const clockNumberColor = - theme.palette.type === 'light' ? theme.palette.text.primary : theme.palette.text.secondary; +export const styles = (theme: Theme) => { + const size = 32; + const clockNumberColor = + theme.palette.mode === 'light' ? theme.palette.text.primary : theme.palette.text.secondary; - return { - root: { - outline: 0, - width: size, - height: size, - userSelect: 'none', - position: 'absolute', - left: `calc((100% - ${size}px) / 2)`, - display: 'inline-flex', - justifyContent: 'center', - alignItems: 'center', - borderRadius: '50%', - color: clockNumberColor, - '&:focused': { - backgroundColor: theme.palette.background.paper, - }, - }, - clockNumberSelected: { - color: theme.palette.primary.contrastText, + return createStyles({ + root: { + outline: 0, + width: size, + height: size, + userSelect: 'none', + position: 'absolute', + left: `calc((100% - ${size}px) / 2)`, + display: 'inline-flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: '50%', + color: clockNumberColor, + '&:focused': { + backgroundColor: theme.palette.background.paper, }, - clockNumberDisabled: { - pointerEvents: 'none', - color: fade(clockNumberColor, 0.2), - }, - }; - }, - { name: 'MuiPickersClockNumber' } -); + }, + clockNumberSelected: { + color: theme.palette.primary.contrastText, + }, + clockNumberDisabled: { + pointerEvents: 'none', + color: alpha(clockNumberColor, 0.2), + }, + }); +}; + +export type ClockNumberClassKey = keyof WithStyles['classes']; + +/** + * @ignore - internal component. + */ +const ClockNumber: React.FC> = (props) => { + const { + disabled, + getClockNumberText, + index, + isInner, + label, + onSelect, + selected, + classes, + } = props; -export const ClockNumber: React.FC = (props) => { - const { disabled, getClockNumberText, index, isInner, label, onSelect, selected } = props; - const classes = useStyles(); const canAutoFocus = useCanAutoFocus(); const ref = React.useRef(null); const className = clsx(classes.root, { @@ -121,4 +132,4 @@ export const ClockNumber: React.FC = (props) => { ); }; -export default ClockNumber; +export default withStyles(styles, { name: 'MuiClockNumber' })(ClockNumber); diff --git a/packages/pickers/lib/src/views/Clock/ClockNumbers.tsx b/packages/material-ui-lab/src/ClockPicker/ClockNumbers.tsx similarity index 90% rename from packages/pickers/lib/src/views/Clock/ClockNumbers.tsx rename to packages/material-ui-lab/src/ClockPicker/ClockNumbers.tsx index 911459af1b550e..43617104061651 100644 --- a/packages/pickers/lib/src/views/Clock/ClockNumbers.tsx +++ b/packages/material-ui-lab/src/ClockPicker/ClockNumbers.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { ClockNumber } from './ClockNumber'; -import { MuiPickersAdapter } from '../../_shared/hooks/useUtils'; -import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; +import ClockNumber from './ClockNumber'; +import { MuiPickersAdapter } from '../internal/pickers/hooks/useUtils'; +import { PickerSelectionState } from '../internal/pickers/hooks/usePickerState'; interface GetHourNumbersOptions { ampm: boolean; @@ -12,6 +12,9 @@ interface GetHourNumbersOptions { utils: MuiPickersAdapter; } +/** + * @ignore - internal component. + */ export const getHourNumbers = ({ ampm, date, @@ -60,7 +63,7 @@ export const getHourNumbers = ({ label={utils.formatNumber(label)} onSelect={() => onChange(hour, 'finish')} getClockNumberText={getClockNumberText} - /> + />, ); } diff --git a/packages/pickers/lib/src/views/Clock/ClockView.tsx b/packages/material-ui-lab/src/ClockPicker/ClockPicker.tsx similarity index 56% rename from packages/pickers/lib/src/views/Clock/ClockView.tsx rename to packages/material-ui-lab/src/ClockPicker/ClockPicker.tsx index 1cce82f6c3e6da..8566dfe8b09b28 100644 --- a/packages/pickers/lib/src/views/Clock/ClockView.tsx +++ b/packages/material-ui-lab/src/ClockPicker/ClockPicker.tsx @@ -1,63 +1,61 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import { Clock } from './Clock'; -import { pipe } from '../../_helpers/utils'; -import { useUtils, useNow } from '../../_shared/hooks/useUtils'; -import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -import { useDefaultProps } from '../../_shared/withDefaultProps'; +import PropTypes from 'prop-types'; +import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; +import Clock from './Clock'; +import { pipe } from '../internal/pickers/utils'; +import { useUtils, useNow, MuiPickersAdapter } from '../internal/pickers/hooks/useUtils'; import { getHourNumbers, getMinutesNumbers } from './ClockNumbers'; -import { useMeridiemMode } from '../../TimePicker/TimePickerToolbar'; -import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; -import { ArrowSwitcher, ExportedArrowSwitcherProps } from '../../_shared/ArrowSwitcher'; +import ArrowSwitcher, { + ExportedArrowSwitcherProps, +} from '../internal/pickers/PickersArrowSwitcher'; import { convertValueToMeridiem, createIsAfterIgnoreDatePart, TimeValidationProps, -} from '../../_helpers/time-utils'; +} from '../internal/pickers/time-utils'; +import { PickerOnChangeFn } from '../internal/pickers/hooks/useViews'; +import { PickerSelectionState } from '../internal/pickers/hooks/usePickerState'; +import { useMeridiemMode } from '../internal/pickers/hooks/date-helpers-hooks'; -export interface ExportedClockViewProps extends TimeValidationProps { +export interface ExportedClockPickerProps extends TimeValidationProps { /** * 12h/24h view for hour selection clock. - * * @default true */ ampm?: boolean; /** * Step over minutes. - * * @default 1 */ minutesStep?: number; /** * Display ampm controls under the clock (instead of in the toolbar). - * * @default false */ ampmInClock?: boolean; /** * Enables keyboard listener for moving between days in calendar. - * * @default currentWrapper !== 'static' */ allowKeyboardControl?: boolean; + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText?: ( + view: 'hours' | 'minutes' | 'seconds', + time: TDate, + adapter: MuiPickersAdapter, + ) => string; } -export interface ClockViewProps - extends ExportedClockViewProps, +export interface ClockPickerProps + extends ExportedClockPickerProps, ExportedArrowSwitcherProps { /** * Selected date @DateIOType. */ date: TDate | null; - /** - * Clock type. - */ - type: 'hours' | 'minutes' | 'seconds'; - /** - * On change date without moving between views @DateIOType. - */ - onDateChange: PickerOnChangeFn; /** * On change callback @DateIOType. */ @@ -76,39 +74,44 @@ export interface ClockViewProps getSecondsClockNumberText?: (secondsText: string) => string; openNextView: () => void; openPreviousView: () => void; + view: 'hours' | 'minutes' | 'seconds'; nextViewAvailable: boolean; previousViewAvailable: boolean; showViewSwitcher?: boolean; } -const muiPickersComponentConfig = { name: 'MuiPickersClockView' }; +export const styles = createStyles({ + arrowSwitcher: { + position: 'absolute', + right: 12, + top: 15, + }, +}); -export const useStyles = makeStyles( - () => ({ - arrowSwitcher: { - position: 'absolute', - right: 12, - top: 15, - }, - }), - muiPickersComponentConfig -); +const getDefaultClockLabelText = ( + view: 'hours' | 'minutes' | 'seconds', + time: TDate, + adapter: MuiPickersAdapter, +) => `Select ${view}. Selected time is ${adapter.format(time, 'fullTime')}`; -function getMinutesAriaText(minute: string) { - return `${minute} minutes`; -} +const getMinutesAriaText = (minute: string) => `${minute} minutes`; const getHoursAriaText = (hour: string) => `${hour} hours`; const getSecondsAriaText = (seconds: string) => `${seconds} seconds`; -export function ClockView(props: ClockViewProps) { +/** + * @ignore - do not document. + */ +function ClockPicker(props: ClockPickerProps & WithStyles) { const { allowKeyboardControl, ampm, ampmInClock, + classes, date, disableIgnoringDatePartForTimeValidation, + getClockLabelText = getDefaultClockLabelText, getHoursClockNumberText = getHoursAriaText, getMinutesClockNumberText = getMinutesAriaText, getSecondsClockNumberText = getSecondsAriaText, @@ -120,7 +123,6 @@ export function ClockView(props: ClockViewProps) { minutesStep = 1, nextViewAvailable, onChange, - onDateChange, openNextView, openPreviousView, previousViewAvailable, @@ -129,22 +131,17 @@ export function ClockView(props: ClockViewProps) { rightArrowIcon, shouldDisableTime, showViewSwitcher, - type, - } = useDefaultProps(props, muiPickersComponentConfig); + view, + } = props; const now = useNow(); const utils = useUtils(); - const classes = useStyles(); const dateOrNow = date || now; - const { meridiemMode, handleMeridiemChange } = useMeridiemMode( - dateOrNow, - ampm, - onDateChange - ); + const { meridiemMode, handleMeridiemChange } = useMeridiemMode(dateOrNow, ampm, onChange); const isTimeDisabled = React.useCallback( - (rawValue: number, type: 'hours' | 'minutes' | 'seconds') => { + (rawValue: number, viewType: 'hours' | 'minutes' | 'seconds') => { if (date === null) { return false; } @@ -152,25 +149,25 @@ export function ClockView(props: ClockViewProps) { const validateTimeValue = (getRequestedTimePoint: (when: 'start' | 'end') => TDate) => { const isAfterComparingFn = createIsAfterIgnoreDatePart( Boolean(disableIgnoringDatePartForTimeValidation), - utils + utils, ); return Boolean( (minTime && isAfterComparingFn(minTime, getRequestedTimePoint('end'))) || (maxTime && isAfterComparingFn(getRequestedTimePoint('start'), maxTime)) || - (shouldDisableTime && shouldDisableTime(rawValue, type)) + (shouldDisableTime && shouldDisableTime(rawValue, viewType)), ); }; - switch (type) { + switch (viewType) { case 'hours': { const hoursWithMeridiem = convertValueToMeridiem(rawValue, meridiemMode, Boolean(ampm)); return validateTimeValue((when: 'start' | 'end') => pipe( (currentDate) => utils.setHours(currentDate, hoursWithMeridiem), (dateWithHours) => utils.setMinutes(dateWithHours, when === 'start' ? 0 : 59), - (dateWithMinutes) => utils.setSeconds(dateWithMinutes, when === 'start' ? 0 : 59) - )(date) + (dateWithMinutes) => utils.setSeconds(dateWithMinutes, when === 'start' ? 0 : 59), + )(date), ); } @@ -178,8 +175,8 @@ export function ClockView(props: ClockViewProps) { return validateTimeValue((when: 'start' | 'end') => pipe( (currentDate) => utils.setMinutes(currentDate, rawValue), - (dateWithMinutes) => utils.setSeconds(dateWithMinutes, when === 'start' ? 0 : 59) - )(date) + (dateWithMinutes) => utils.setSeconds(dateWithMinutes, when === 'start' ? 0 : 59), + )(date), ); case 'seconds': @@ -198,11 +195,11 @@ export function ClockView(props: ClockViewProps) { minTime, shouldDisableTime, utils, - ] + ], ); const viewProps = React.useMemo(() => { - switch (type) { + switch (view) { case 'hours': { const handleHoursChange = (value: number, isFinish?: PickerSelectionState) => { const valueWithMeridiem = convertValueToMeridiem(value, meridiemMode, Boolean(ampm)); @@ -265,7 +262,7 @@ export function ClockView(props: ClockViewProps) { throw new Error('You must provide the type for ClockView'); } }, [ - type, + view, utils, date, ampm, @@ -296,13 +293,13 @@ export function ClockView(props: ClockViewProps) { /> )} - date={date} ampmInClock={ampmInClock} - // @ts-expect-error FIX ME - onDateChange={onDateChange} - type={type} + type={view} ampm={ampm} + // @ts-expect-error TODO figure out this weird error + getClockLabelText={getClockLabelText} minutesStep={minutesStep} allowKeyboardControl={allowKeyboardControl} isTimeDisabled={isTimeDisabled} @@ -314,12 +311,130 @@ export function ClockView(props: ClockViewProps) { ); } -ClockView.propTypes = { +(ClockPicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ ampm: PropTypes.bool, - date: PropTypes.object, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * @ignore + */ + classes: PropTypes.object.isRequired, + /** + * Selected date @DateIOType. + */ + date: PropTypes.any, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get clock number aria-text for hours. + */ + getHoursClockNumberText: PropTypes.func, + /** + * Get clock number aria-text for minutes. + */ + getMinutesClockNumberText: PropTypes.func, + /** + * Get clock number aria-text for seconds. + */ + getSecondsClockNumberText: PropTypes.func, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * Max time acceptable time. + * For input validation date part of passed object will be ignored if `disableIgnoringDatePartForTimeValidation` not specified. + */ + maxTime: PropTypes.any, + /** + * Min time acceptable time. + * For input validation date part of passed object will be ignored if `disableIgnoringDatePartForTimeValidation` not specified. + */ + minTime: PropTypes.any, + /** + * Step over minutes. + * @default 1 + */ minutesStep: PropTypes.number, + /** + * @ignore + */ + nextViewAvailable: PropTypes.bool.isRequired, + /** + * On change callback @DateIOType. + */ onChange: PropTypes.func.isRequired, - type: PropTypes.oneOf(['minutes', 'hours', 'seconds']).isRequired, -} as any; + /** + * @ignore + */ + openNextView: PropTypes.func.isRequired, + /** + * @ignore + */ + openPreviousView: PropTypes.func.isRequired, + /** + * @ignore + */ + previousViewAvailable: PropTypes.bool.isRequired, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * @ignore + */ + showViewSwitcher: PropTypes.bool, + /** + * @ignore + */ + view: PropTypes.oneOf(['hours', 'minutes', 'seconds']).isRequired, +}; -ClockView.displayName = 'ClockView'; +export default withStyles(styles, { name: 'MuiPickersClockView' })(ClockPicker) as ( + props: ClockPickerProps, +) => JSX.Element; diff --git a/packages/material-ui-lab/src/ClockPicker/ClockPickerStandalone.test.tsx b/packages/material-ui-lab/src/ClockPicker/ClockPickerStandalone.test.tsx new file mode 100644 index 00000000000000..c47f8b3c75b292 --- /dev/null +++ b/packages/material-ui-lab/src/ClockPicker/ClockPickerStandalone.test.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { createMount, describeConformance } from 'test/utils'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import ClockPicker from '@material-ui/lab/ClockPicker'; + +describe('', () => { + const mount = createMount(); + + const localizedMount = (node: React.ReactNode) => { + return mount({node}); + }; + + describeConformance( {}} />, () => ({ + classes: {}, + inheritComponent: 'div', + mount: localizedMount, + refInstanceof: window.HTMLDivElement, + // cannot test reactTestRenderer because of required context + skip: ['componentProp', 'propsSpread', 'reactTestRenderer'], + })); +}); diff --git a/packages/material-ui-lab/src/ClockPicker/ClockPickerStandalone.tsx b/packages/material-ui-lab/src/ClockPicker/ClockPickerStandalone.tsx new file mode 100644 index 00000000000000..e08b6f3f961700 --- /dev/null +++ b/packages/material-ui-lab/src/ClockPicker/ClockPickerStandalone.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import ClockPicker, { ClockPickerProps } from './ClockPicker'; +import { TimePickerView } from '../internal/pickers/typings/Views'; +import PickerView from '../internal/pickers/Picker/PickerView'; +import { useViews } from '../internal/pickers/hooks/useViews'; + +export interface ClockPickerStandaloneProps + extends Omit< + ClockPickerProps, + 'view' | 'openNextView' | 'openPreviousView' | 'nextViewAvailable' | 'previousViewAvailable' + > { + /** Controlled clock view. */ + view?: TimePickerView; + /** Available views for clock. */ + views?: TimePickerView[]; + /** Callback fired on view change. */ + onViewChange?: (view: TimePickerView) => void; + /** Initially opened view. */ + openTo?: TimePickerView; + className?: string; +} + +/** + * Wrapping public API for better standalone usage of './ClockPicker' + * @ignore - internal component. + */ +export default React.forwardRef(function ClockPickerStandalone( + props: ClockPickerStandaloneProps, + ref: React.Ref, +) { + const { + view, + openTo, + className, + onViewChange, + views = ['hours', 'minutes'] as TimePickerView[], + ...other + } = props; + + const { openView, setOpenView, nextView, previousView } = useViews({ + view, + views, + openTo, + onViewChange, + onChange: other.onChange, + }); + + return ( + + setOpenView(nextView)} + openPreviousView={() => setOpenView(previousView)} + {...other} + /> + + ); +}) as ( + props: ClockPickerStandaloneProps & React.RefAttributes, +) => JSX.Element; diff --git a/packages/pickers/lib/src/views/Clock/ClockPointer.tsx b/packages/material-ui-lab/src/ClockPicker/ClockPointer.tsx similarity index 86% rename from packages/pickers/lib/src/views/Clock/ClockPointer.tsx rename to packages/material-ui-lab/src/ClockPicker/ClockPointer.tsx index 0cb7f0c2d84f06..9a39a6e35de36d 100644 --- a/packages/pickers/lib/src/views/Clock/ClockPointer.tsx +++ b/packages/material-ui-lab/src/ClockPicker/ClockPointer.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import clsx from 'clsx'; import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles'; -import { ClockViewType } from '../../constants/ClockType'; +import { ClockViewType } from '../internal/pickers/constants/ClockType'; export const styles = (theme: Theme) => createStyles({ @@ -32,6 +32,8 @@ export const styles = (theme: Theme) => }, }); +export type ClockPointerClassKey = keyof WithStyles['classes']; + export interface ClockPointerProps extends React.HTMLProps, WithStyles { @@ -41,10 +43,13 @@ export interface ClockPointerProps type: ClockViewType; } +/** + * @ignore - internal component. + */ class ClockPointer extends React.Component { - public static getDerivedStateFromProps = ( + static getDerivedStateFromProps = ( nextProps: ClockPointerProps, - state: ClockPointer['state'] + state: ClockPointer['state'], ) => { if (nextProps.type !== state.previousType) { return { @@ -59,13 +64,13 @@ class ClockPointer extends React.Component { }; }; - public state = { + state = { toAnimateTransform: false, // eslint-disable-next-line react/no-unused-state previousType: undefined, }; - public getAngleStyle = () => { + getAngleStyle = () => { const { value, isInner, type } = this.props; const max = type === 'hours' ? 12 : 60; @@ -81,7 +86,7 @@ class ClockPointer extends React.Component { }; }; - public render() { + render() { const { classes, hasSelected, isInner, type, value, ...other } = this.props; return ( @@ -103,5 +108,5 @@ class ClockPointer extends React.Component { } export default withStyles(styles, { - name: 'MuiPickersClockPointer', -})(ClockPointer as React.ComponentType); + name: 'MuiClockPointer', +})(ClockPointer); diff --git a/packages/material-ui-lab/src/ClockPicker/index.ts b/packages/material-ui-lab/src/ClockPicker/index.ts new file mode 100644 index 00000000000000..aa84e211e250fe --- /dev/null +++ b/packages/material-ui-lab/src/ClockPicker/index.ts @@ -0,0 +1,5 @@ +export { default } from './ClockPickerStandalone'; + +export type ClockPickerProps = import('./ClockPickerStandalone').ClockPickerStandaloneProps< + TDate +>; diff --git a/packages/material-ui-lab/src/DatePicker/DatePicker.spec.tsx b/packages/material-ui-lab/src/DatePicker/DatePicker.spec.tsx new file mode 100644 index 00000000000000..1178f50e368e8d --- /dev/null +++ b/packages/material-ui-lab/src/DatePicker/DatePicker.spec.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import moment, { Moment } from 'moment'; +import { DatePicker, StaticDatePicker, DayPicker, PickersDay } from '@material-ui/lab'; +import DateFnsAdapter from '../dateAdapter/date-fns'; +import MomentAdapter from '../dateAdapter/moment'; + +// Allows to set date type right with generic JSX syntax + + value={new Date()} + onChange={(date) => date?.getDate()} + renderInput={() => } +/>; + +// Throws error if passed value is invalid + + // @ts-expect-error Value is invalid + value={moment()} + onChange={(date) => date?.getDate()} + renderInput={() => } +/>; + +// Inference from the state +const InferTest = () => { + const [date, setDate] = React.useState(moment()); + + return ( + setDate(date)} renderInput={() => } /> + ); +}; + +// Infer value type from the dateAdapter + console.log(date)} + renderInput={() => } + dateAdapter={new MomentAdapter()} +/>; + +// Conflict between value type and date adapter causes error + console.log(date)} + renderInput={() => } + // @ts-expect-error + dateAdapter={new DateFnsAdapter()} +/>; + +// Conflict between explicit generic type and date adapter causes error + + value={moment()} + onChange={(date) => console.log(date)} + renderInput={() => } + // @ts-expect-error + dateAdapter={new LuxonAdapter()} +/>; + +// Allows inferring for side props + {day.format('D')} } + onChange={(date) => date?.set({ second: 0 })} + renderInput={() => } +/>; + +// External components are generic as well + + view="date" + views={['date']} + date={moment()} + minDate={moment()} + maxDate={moment()} + onChange={(date) => date?.format()} +/>; + + + day={new Date()} + allowSameDateSelection + outsideCurrentMonth + onDaySelect={(date) => date?.getDay()} +/>; + +// Edge case and known issue. When the passed `value` is not a date type +// We cannot infer the type properly without explicit generic type or `dateAdapter` prop +// So in this case it is expected that type will be the type of `value` as for now + + // getDate is never + // @ts-expect-error + date?.getDate() + } + renderInput={() => } +/>; + +{ + // Allows to pass the wrapper-specific props only to the proper wrapper + date?.getDate()} + renderInput={() => } + displayStaticWrapperAs="desktop" + />; + + date?.getDate()} + renderInput={() => } + // @ts-expect-error + displayStaticWrapperAs="desktop" + />; +} diff --git a/packages/material-ui-lab/src/DatePicker/DatePicker.test.tsx b/packages/material-ui-lab/src/DatePicker/DatePicker.test.tsx new file mode 100644 index 00000000000000..6248539d0055ce --- /dev/null +++ b/packages/material-ui-lab/src/DatePicker/DatePicker.test.tsx @@ -0,0 +1,521 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import TextField from '@material-ui/core/TextField'; +import { fireEvent, screen, waitFor } from 'test/utils'; +import PickersDay from '@material-ui/lab/PickersDay'; +import CalendarSkeleton from '@material-ui/lab/PickersCalendarSkeleton'; +import DatePicker from '@material-ui/lab/DatePicker'; +import MobileDatePicker from '@material-ui/lab/MobileDatePicker'; +import DesktopDatePicker from '@material-ui/lab/DesktopDatePicker'; +import StaticDatePicker from '@material-ui/lab/StaticDatePicker'; +import { + createPickerRender, + FakeTransitionComponent, + adapterToUse, + getByMuiTest, + getAllByMuiTest, + queryAllByMuiTest, + openDesktopPicker, + openMobilePicker, +} from '../internal/pickers/test-utils'; + +describe('', () => { + const render = createPickerRender({ strict: false }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('render proper month', () => { + render( + {}} + renderInput={(params) => } + />, + ); + + expect(screen.getByText('January')).toBeVisible(); + expect(screen.getByText('2019')).toBeVisible(); + expect(getAllByMuiTest('day')).to.have.length(31); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('desktop Mode – Accepts date on day button click', () => { + const onChangeMock = spy(); + + render( + } + />, + ); + + openDesktopPicker(); + + fireEvent.click(screen.getByLabelText('Jan 2, 2019')); + expect(onChangeMock.callCount).to.equal(1); + + expect(screen.queryByRole('dialog')).to.equal(null); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('mobile mode – Accepts date on `OK` button click', () => { + const onChangeMock = spy(); + render( + } + />, + ); + + openMobilePicker(); + + fireEvent.click(screen.getByLabelText('Jan 2, 2019')); + expect(onChangeMock.callCount).to.equal(1); + expect(screen.queryByRole('dialog')).not.to.equal(null); + + fireEvent.click(screen.getByText(/ok/i)); + // TODO revisit calling onChange twice. Now it is expected for mobile mode. + expect(onChangeMock.callCount).to.equal(2); + expect(screen.queryByRole('dialog')).to.equal(null); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('switches between months', () => { + render( + {}} + renderInput={(params) => } + />, + ); + + expect(getByMuiTest('calendar-month-text')).to.have.text('January'); + + fireEvent.click(screen.getByLabelText('next month')); + fireEvent.click(screen.getByLabelText('next month')); + + fireEvent.click(screen.getByLabelText('previous month')); + fireEvent.click(screen.getByLabelText('previous month')); + fireEvent.click(screen.getByLabelText('previous month')); + + expect(getByMuiTest('calendar-month-text')).to.have.text('December'); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('selects the closest enabled date if selected date is disabled', () => { + const onChangeMock = spy(); + + render( + } + maxDate={adapterToUse.date('2018-01-01T00:00:00.000')} + />, + ); + + expect(getByMuiTest('calendar-year-text')).to.have.text('2018'); + expect(getByMuiTest('calendar-month-text')).to.have.text('January'); + + // onChange must be dispatched with newly selected date + expect(onChangeMock.calledWith(adapterToUse.date('2018-01-01T00:00:00.000'))).to.be.equal(true); + }); + + it('allows to change only year', () => { + const onChangeMock = spy(); + render( + } + />, + ); + + fireEvent.click(screen.getByLabelText(/switch to year view/i)); + fireEvent.click(screen.getByText('2010', { selector: 'button' })); + + expect(getByMuiTest('calendar-year-text')).to.have.text('2010'); + expect(onChangeMock.callCount).to.equal(1); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('allows to select edge years from list', () => { + render( + {}} + openTo="year" + minDate={new Date('2000-01-01T00:00:00.000')} + maxDate={new Date('2010-01-01T00:00:00.000')} + renderInput={(params) => } + />, + ); + + fireEvent.click(screen.getByText('2010', { selector: 'button' })); + expect(getByMuiTest('datepicker-toolbar-date')).to.have.text('Fri, Jan 1'); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("doesn't close picker on selection in Mobile mode", () => { + render( + {}} + renderInput={(params) => } + />, + ); + + fireEvent.click(screen.getByRole('textbox')); + fireEvent.click(screen.getByLabelText('Jan 2, 2018')); + + expect(screen.queryByRole('dialog')).toBeVisible(); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('closes picker on selection in Desktop mode', async () => { + render( + {}} + renderInput={(params) => } + />, + ); + + fireEvent.click(screen.getByLabelText('Choose date, selected date is Jan 1, 2018')); + + await waitFor(() => screen.getByRole('dialog')); + fireEvent.click(screen.getByLabelText('Jan 2, 2018')); + + expect(screen.queryByRole('dialog')).to.equal(null); + }); + + it('prop `clearable` - renders clear button in Mobile mode', () => { + const onChangeMock = spy(); + render( + } + />, + ); + + openMobilePicker(); + fireEvent.click(screen.getByText('Clear')); + + expect(onChangeMock.calledWith(null)).to.be.equal(true); + expect(screen.queryByRole('dialog')).to.equal(null); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("prop `disableCloseOnSelect` – if `true` doesn't close picker", () => { + render( + {}} + renderInput={(params) => } + />, + ); + + openDesktopPicker(); + fireEvent.click(screen.getByLabelText('Jan 2, 2018')); + + expect(screen.queryByRole('dialog')).toBeVisible(); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('does not call onChange if same date selected', async () => { + const onChangeMock = spy(); + + render( + } + />, + ); + + fireEvent.click(screen.getByLabelText('Choose date, selected date is Jan 1, 2018')); + await waitFor(() => screen.getByRole('dialog')); + + fireEvent.click(screen.getByLabelText('Jan 1, 2018')); + expect(onChangeMock.callCount).to.equal(0); + }); + + it('allows to change selected date from the input according to `format`', () => { + const onChangeMock = spy(); + render( + } + label="Masked input" + inputFormat="dd/MM/yyyy" + value={new Date('2018-01-01T00:00:00.000Z')} + onChange={onChangeMock} + InputAdornmentProps={{ + disableTypography: true, + }} + />, + ); + + fireEvent.change(screen.getByRole('textbox'), { + target: { + value: '10/11/2018', + }, + }); + + expect(screen.getByRole('textbox')).to.have.value('10/11/2018'); + expect(onChangeMock.callCount).to.equal(1); + }); + + it('prop `showToolbar` – renders toolbar in desktop mode', () => { + render( + {}} + TransitionComponent={FakeTransitionComponent} + value={adapterToUse.date('2018-01-01T00:00:00.000')} + renderInput={(params) => } + />, + ); + + expect(getByMuiTest('picker-toolbar')).toBeVisible(); + }); + + it('prop `toolbarTitle` – should render title from the prop', () => { + render( + } + open + toolbarTitle="test" + label="something" + onChange={() => {}} + value={adapterToUse.date('2018-01-01T00:00:00.000')} + />, + ); + + expect(getByMuiTest('picker-toolbar-title').textContent).to.equal('test'); + }); + + it('prop `toolbarTitle` – should use label if no toolbar title', () => { + render( + {}} + renderInput={(params) => } + value={adapterToUse.date('2018-01-01T00:00:00.000')} + />, + ); + + expect(getByMuiTest('picker-toolbar-title').textContent).to.equal('Default label'); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('prop `toolbarFormat` – should format toolbar according to passed format', () => { + render( + } + open + onChange={() => {}} + toolbarFormat="MMMM" + value={adapterToUse.date('2018-01-01T00:00:00.000')} + />, + ); + + expect(getByMuiTest('datepicker-toolbar-date').textContent).to.equal('January'); + }); + + it('prop `showTodayButton` – accept current date when "today" button is clicked', () => { + const onCloseMock = spy(); + const onChangeMock = spy(); + render( + } + showTodayButton + cancelText="stream" + onClose={onCloseMock} + onChange={onChangeMock} + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + DialogProps={{ TransitionComponent: FakeTransitionComponent }} + />, + ); + + fireEvent.click(screen.getByRole('textbox')); + fireEvent.click(screen.getByText(/today/i)); + + expect(onCloseMock.callCount).to.equal(1); + expect(onChangeMock.callCount).to.equal(1); + }); + + it('ref - should forwardRef to text field', () => { + const Component = () => { + const ref = React.useRef(null); + const focusPicker = () => { + if (ref.current) { + ref.current.focus(); + expect(ref.current.id).to.equal('test-focusing-picker'); + } else { + throw new Error('Ref must be available'); + } + }; + + return ( + + {}} + renderInput={(params) => } + /> + + + ); + }; + + render(); + fireEvent.click(screen.getByText('test')); + }); + + it('prop `shouldDisableYear` – disables years dynamically', () => { + render( + } + openTo="year" + onChange={() => {}} + // getByRole() with name attribute is too slow, so restrict the number of rendered years + minDate={new Date('2025-01-01T00:00:00.000')} + maxDate={new Date('2035-01-01T00:00:00.000')} + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + shouldDisableYear={(year) => adapterToUse.getYear(year) === 2030} + />, + ); + + const getYearButton = (year: number) => + screen.getByText(year.toString(), { selector: 'button' }); + + expect(getYearButton(2029)).not.to.have.attribute('disabled'); + expect(getYearButton(2030)).to.have.attribute('disabled'); + expect(getYearButton(2031)).not.to.have.attribute('disabled'); + }); + + it('prop `onMonthChange` – dispatches callback when months switching', () => { + const onMonthChangeMock = spy(); + render( + } + onChange={() => {}} + onMonthChange={onMonthChangeMock} + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + />, + ); + + fireEvent.click(screen.getByLabelText('next month')); + expect(onMonthChangeMock.callCount).to.equal(1); + }); + + it('prop `loading` – displays default loading indicator', () => { + render( + } + onChange={() => {}} + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + />, + ); + + expect(queryAllByMuiTest(document.body, 'day')).to.have.length(0); + expect(getByMuiTest('loading-progress')).toBeVisible(); + }); + + it('prop `renderLoading` – displays custom loading indicator', () => { + render( + } + open + onChange={() => {}} + renderInput={(params) => } + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + />, + ); + + expect(screen.queryByTestId('loading-progress')).to.equal(null); + expect(screen.getByTestId('custom-loading')).toBeVisible(); + }); + + it('prop `ToolbarComponent` – render custom toolbar component', () => { + render( + } + open + value={new Date()} + onChange={() => {}} + ToolbarComponent={() =>
} + />, + ); + + expect(screen.getByTestId('custom-toolbar')).toBeVisible(); + }); + + it('prop `renderDay` – renders custom day', () => { + render( + } + open + value={adapterToUse.date('2018-01-01T00:00:00.000')} + onChange={() => {}} + renderDay={(day, _selected, DayComponentProps) => ( + + )} + />, + ); + + expect(screen.getAllByTestId('test-day')).to.have.length(31); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('prop `defaultCalendarMonth` – opens on provided month if date is `null`', () => { + render( + } + open + value={null} + onChange={() => {}} + defaultCalendarMonth={new Date('2018-07-01T00:00:00.000')} + />, + ); + + expect(screen.getByText('July')).toBeVisible(); + }); +}); diff --git a/packages/material-ui-lab/src/DatePicker/DatePicker.tsx b/packages/material-ui-lab/src/DatePicker/DatePicker.tsx new file mode 100644 index 00000000000000..655c7aa73b0bb7 --- /dev/null +++ b/packages/material-ui-lab/src/DatePicker/DatePicker.tsx @@ -0,0 +1,298 @@ +import PropTypes from 'prop-types'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import DatePickerToolbar from './DatePickerToolbar'; +import type { WithViewsProps } from '../internal/pickers/Picker/SharedPickerProps'; +import { ResponsiveWrapper } from '../internal/pickers/wrappers/ResponsiveWrapper'; +import { + useParsedDate, + OverrideParsableDateProps, +} from '../internal/pickers/hooks/date-helpers-hooks'; +import type { ExportedDayPickerProps } from '../DayPicker/DayPicker'; +import { MobileWrapper, SomeWrapper } from '../internal/pickers/wrappers/Wrapper'; +import { makeValidationHook, ValidationProps } from '../internal/pickers/hooks/useValidation'; +import { + ParsableDate, + defaultMinDate, + defaultMaxDate, +} from '../internal/pickers/constants/prop-types'; +import { + makePickerWithStateAndWrapper, + AllPickerProps, + SharedPickerProps, +} from '../internal/pickers/Picker/makePickerWithState'; +import { + getFormatAndMaskByViews, + DateValidationError, + validateDate, +} from '../internal/pickers/date-utils'; + +export type DatePickerView = 'year' | 'date' | 'month'; + +export interface BaseDatePickerProps + extends WithViewsProps<'year' | 'date' | 'month'>, + ValidationProps, + OverrideParsableDateProps, 'minDate' | 'maxDate'> {} + +export const datePickerConfig = { + useValidation: makeValidationHook< + DateValidationError, + ParsableDate, + BaseDatePickerProps + >(validateDate), + DefaultToolbarComponent: DatePickerToolbar, + useInterceptProps: ({ + openTo = 'date', + views = ['year', 'date'], + minDate: __minDate = defaultMinDate, + maxDate: __maxDate = defaultMaxDate, + ...other + }: AllPickerProps>) => { + const utils = useUtils(); + const minDate = useParsedDate(__minDate); + const maxDate = useParsedDate(__maxDate); + + return { + views, + openTo, + minDate, + maxDate, + ...getFormatAndMaskByViews(views, utils), + ...other, + }; + }, +}; + +export type DatePickerGenericComponent = ( + props: BaseDatePickerProps & SharedPickerProps, +) => JSX.Element; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const DatePicker = makePickerWithStateAndWrapper>(ResponsiveWrapper, { + name: 'MuiDatePicker', + ...datePickerConfig, +}) as DatePickerGenericComponent; + +(DatePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * "CANCEL" Text message + * @default "CANCEL" + */ + cancelText: PropTypes.node, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * If `true`, it shows the clear action in the picker dialog. + * @default false + */ + clearable: PropTypes.bool, + /** + * "CLEAR" Text message + * @default "CLEAR" + */ + clearText: PropTypes.node, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * "OK" button text. + * @default "OK" + */ + okText: PropTypes.node, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. + * @default false + */ + showTodayButton: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * "TODAY" Text message + * @default "TODAY" + */ + todayText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), +}; + +export type DatePickerProps = React.ComponentProps; + +export default DatePicker; diff --git a/packages/material-ui-lab/src/DatePicker/DatePickerKeyboard.test.tsx b/packages/material-ui-lab/src/DatePicker/DatePickerKeyboard.test.tsx new file mode 100644 index 00000000000000..1a0c71273e0b78 --- /dev/null +++ b/packages/material-ui-lab/src/DatePicker/DatePickerKeyboard.test.tsx @@ -0,0 +1,275 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { isWeekend } from 'date-fns'; +import TextField from '@material-ui/core/TextField'; +import { fireEvent, screen, act } from 'test/utils'; +import DesktopDatePicker, { DesktopDatePickerProps } from '@material-ui/lab/DesktopDatePicker'; +import StaticDatePicker from '@material-ui/lab/StaticDatePicker'; +import { createPickerRender } from '../internal/pickers/test-utils'; +import { MakeOptional } from '../internal/pickers/typings/helpers'; + +function TestKeyboardDatePicker( + PickerProps: MakeOptional, +) { + const [value, setValue] = React.useState( + PickerProps.value ?? new Date('2019-01-01T00:00:00.000'), + ); + + return ( + setValue(newDate)} + renderInput={(props) => } + {...PickerProps} + /> + ); +} + +describe(' keyboard interactions', () => { + const render = createPickerRender({ strict: false }); + + context('input', () => { + it('allows to change selected date from the input according to `format`', () => { + const onChangeMock = spy(); + render(); + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '10/11/2018' }, + }); + + expect(screen.getByRole('textbox')).to.have.value('10/11/2018'); + expect(onChangeMock.callCount).to.equal(1); + }); + + it("doesn't accept invalid date format", () => { + render(); + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '01' }, + }); + expect(screen.getByRole('textbox')).to.have.attr('aria-invalid', 'true'); + }); + + it('removes invalid state when chars are cleared from invalid input', () => { + render(); + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '01' }, + }); + expect(screen.getByRole('textbox')).to.have.attr('aria-invalid', 'true'); + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '' }, + }); + expect(screen.getByRole('textbox')).to.have.attr('aria-invalid', 'false'); + }); + + it('renders correct format helper text and placeholder', () => { + render( + } + />, + ); + + const helperText = document.getElementById('test-helper-text'); + expect(helperText).to.have.text('yyyy'); + + expect(screen.getByRole('textbox')).to.have.attr('placeholder', 'yyyy'); + }); + + it('correctly input dates according to the input mask', () => { + render(); + const input = screen.getByRole('textbox'); + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '011' }, + }); + expect(input).to.have.value('01/1'); + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '01102019' }, + }); + expect(input).to.have.value('01/10/2019'); + }); + + it('prop `disableMaskedInput` – disables mask and allows to input anything to the field', () => { + render(); + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: 'any text' }, + }); + + const input = screen.getByRole('textbox'); + expect(input).to.have.value('any text'); + expect(input).to.have.attr('aria-invalid', 'true'); + }); + + it('prop `disableMaskedInput` – correctly parses date string when mask is disabled', () => { + const onChangeMock = spy(); + render(); + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: '01/10/2019' }, + }); + + const input = screen.getByRole('textbox'); + expect(input).to.have.value('01/10/2019'); + expect(input).to.have.attribute('aria-invalid', 'false'); + expect(onChangeMock.callCount).to.equal(1); + }); + }); + + context('Calendar keyboard navigation', () => { + beforeEach(() => { + // Important: Use here in order to avoid async waiting for focus because of packages/material-ui-lab/src/internal/pickers/hooks/useCanAutoFocus.tsx logic + render( + {}} + renderInput={(params) => } + />, + ); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('autofocus selected day on mount', () => { + expect(screen.getByLabelText('Aug 13, 2020')).toHaveFocus(); + }); + + [ + { keyCode: 35, key: 'End', expectFocusedDay: 'Aug 15, 2020' }, + { keyCode: 36, key: 'Home', expectFocusedDay: 'Aug 9, 2020' }, + { keyCode: 37, key: 'ArrowLeft', expectFocusedDay: 'Aug 12, 2020' }, + { keyCode: 38, key: 'ArrowUp', expectFocusedDay: 'Aug 6, 2020' }, + { keyCode: 39, key: 'ArrowRight', expectFocusedDay: 'Aug 14, 2020' }, + { keyCode: 40, key: 'ArrowDown', expectFocusedDay: 'Aug 20, 2020' }, + ].forEach(({ key, keyCode, expectFocusedDay }) => { + it(key, () => { + fireEvent.keyDown(document.body, { force: true, keyCode, key }); + + expect(document.activeElement).toHaveAccessibleName(expectFocusedDay); + }); + }); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("doesn't allow to select disabled date from keyboard", async () => { + render( + {}} + renderInput={(params) => } + />, + ); + + expect(document.activeElement).to.have.attr('aria-label', 'Aug 13, 2020'); + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 3; i++) { + fireEvent.keyDown(document.body, { force: true, keyCode: 37, key: 'ArrowLeft' }); + } + + // leaves focus on the same date + expect(document.activeElement).to.have.attr('aria-label', 'Aug 13, 2020'); + }); + + context('YearPicker keyboard navigation', () => { + [ + { keyCode: 37, key: 'ArrowLeft', expectFocusedYear: '2019' }, + { keyCode: 38, key: 'ArrowUp', expectFocusedYear: '2016' }, + { keyCode: 39, key: 'ArrowRight', expectFocusedYear: '2021' }, + { keyCode: 40, key: 'ArrowDown', expectFocusedYear: '2024' }, + ].forEach(({ key, keyCode, expectFocusedYear }) => { + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip(key, () => { + render( + {}} + renderInput={(params) => } + />, + ); + + fireEvent.keyDown(document.body, { force: true, keyCode, key }); + + expect(document.activeElement).to.have.text(expectFocusedYear); + }); + }); + }); + + context('input validaiton', () => { + [ + { expectedError: 'invalidDate', props: {}, input: 'invalidText' }, + { expectedError: 'disablePast', props: { disablePast: true }, input: '01/01/1900' }, + { expectedError: 'disableFuture', props: { disableFuture: true }, input: '01/01/2050' }, + { expectedError: 'minDate', props: { minDate: new Date('01/01/2000') }, input: '01/01/1990' }, + { expectedError: 'maxDate', props: { maxDate: new Date('01/01/2000') }, input: '01/01/2010' }, + { + expectedError: 'shouldDisableDate', + props: { shouldDisableDate: isWeekend }, + input: '04/25/2020', + }, + ].forEach(({ props, input, expectedError }) => { + it(`dispatches ${expectedError} error`, () => { + const onErrorMock = spy(); + // we are running validation on value change + function DatePickerInput() { + const [date, setDate] = React.useState(null); + + return ( + + value={date} + onError={onErrorMock} + onChange={(newDate) => setDate(newDate)} + renderInput={(inputProps) => } + {...props} + /> + ); + } + + render(); + + fireEvent.change(screen.getByRole('textbox'), { + target: { + value: input, + }, + }); + + expect(onErrorMock.calledWith(expectedError)).to.be.equal(true); + }); + }); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('Opens calendar by keydown on the open button', () => { + render(); + const openButton = screen.getByLabelText(/choose date/i); + + act(() => { + openButton.focus(); + }); + + fireEvent.keyDown(openButton, { + key: 'Enter', + keyCode: 13, + }); + + expect(screen.queryByRole('dialog')).toBeVisible(); + }); +}); diff --git a/packages/material-ui-lab/src/DatePicker/DatePickerLocalization.test.tsx b/packages/material-ui-lab/src/DatePicker/DatePickerLocalization.test.tsx new file mode 100644 index 00000000000000..d25b9ee719d347 --- /dev/null +++ b/packages/material-ui-lab/src/DatePicker/DatePickerLocalization.test.tsx @@ -0,0 +1,127 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import fr from 'date-fns/locale/fr'; +import deLocale from 'date-fns/locale/de'; +import enLocale from 'date-fns/locale/en-US'; +import TextField from '@material-ui/core/TextField'; +import MobileDatePicker from '@material-ui/lab/MobileDatePicker'; +import DesktopDatePicker, { DesktopDatePickerProps } from '@material-ui/lab/DesktopDatePicker'; +import { fireEvent, screen } from 'test/utils'; +import { adapterToUse, getByMuiTest, createPickerRender } from '../internal/pickers/test-utils'; + +describe(' localization', () => { + const render = createPickerRender({ strict: false, locale: fr }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('datePicker localized format for year view', () => { + render( + } + value={adapterToUse.date('2018-01-01T00:00:00.000')} + onChange={() => {}} + views={['year']} + />, + ); + + expect(screen.getByRole('textbox')).to.have.value('2018'); + + fireEvent.click(screen.getByLabelText(/Choose date/)); + expect(getByMuiTest('datepicker-toolbar-date').textContent).to.equal('2018'); + }); + + it('datePicker localized format for year+month view', () => { + render( + } + value={adapterToUse.date('2018-01-01T00:00:00.000')} + onChange={() => {}} + views={['year', 'month']} + />, + ); + + expect(screen.getByRole('textbox')).to.have.value('janvier 2018'); + + fireEvent.click(screen.getByLabelText(/Choose date/)); + expect(getByMuiTest('datepicker-toolbar-date').textContent).to.equal('janvier'); + }); + + it('datePicker localized format for year+month+date view', () => { + render( + {}} + renderInput={(params) => } + value={adapterToUse.date('2018-01-01T00:00:00.000')} + views={['year', 'month', 'date']} + />, + ); + + expect(screen.getByRole('textbox')).to.have.value('01/01/2018'); + + fireEvent.click(screen.getByLabelText(/Choose date/)); + expect(getByMuiTest('datepicker-toolbar-date').textContent).to.equal('1 janvier'); + }); + + describe('input validation', () => { + interface FormProps { + Picker: React.ElementType; + PickerProps: Partial; + } + + const Form = (props: FormProps) => { + const { Picker, PickerProps } = props; + const [value, setValue] = React.useState(new Date('01/01/2020')); + + return ( + } + value={value} + {...PickerProps} + /> + ); + }; + + const tests = [ + { + locale: 'en-US', + valid: 'January 2020', + invalid: 'Januar 2020', + dateFnsLocale: enLocale, + }, + { + locale: 'de', + valid: 'Januar 2020', + invalid: 'Janua 2020', + dateFnsLocale: deLocale, + }, + ]; + + tests.forEach(({ valid, invalid, locale, dateFnsLocale }) => { + const localizedRender = createPickerRender({ strict: false, locale: dateFnsLocale }); + + it(`${locale}: should set invalid`, () => { + localizedRender( +
, + ); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: invalid } }); + + expect(input).to.have.attribute('aria-invalid', 'true'); + }); + + it(`${locale}: should set to valid when was invalid`, () => { + localizedRender( + , + ); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: invalid } }); + fireEvent.change(input, { target: { value: valid } }); + + expect(input).to.have.attribute('aria-invalid', 'false'); + }); + }); + }); +}); diff --git a/packages/material-ui-lab/src/DatePicker/DatePickerToolbar.tsx b/packages/material-ui-lab/src/DatePicker/DatePickerToolbar.tsx new file mode 100644 index 00000000000000..24e1e107f9a960 --- /dev/null +++ b/packages/material-ui-lab/src/DatePicker/DatePickerToolbar.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import Typography from '@material-ui/core/Typography'; +import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; +import PickerToolbar from '../internal/pickers/PickersToolbar'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import { isYearAndMonthViews, isYearOnlyView } from '../internal/pickers/date-utils'; +import { DatePickerView } from '../internal/pickers/typings/Views'; +import { ToolbarComponentProps } from '../internal/pickers/typings/BasePicker'; + +export const styles = createStyles({ + root: {}, + dateTitleLandscape: { + margin: 'auto 16px auto auto', + }, + penIcon: { + position: 'relative', + top: 4, + }, +}); +export type DatePickerToolbarClassKey = keyof WithStyles['classes']; + +/** + * @ignore - internal component. + */ +const DatePickerToolbar: React.FC> = ({ + classes, + date, + isLandscape, + isMobileKeyboardViewOpen, + onChange, + toggleMobileKeyboardView, + toolbarFormat, + toolbarPlaceholder = '––', + toolbarTitle = 'SELECT DATE', + views, + ...other +}) => { + const utils = useUtils(); + + const dateText = React.useMemo(() => { + if (!date) { + return toolbarPlaceholder; + } + + if (toolbarFormat) { + return utils.formatByString(date, toolbarFormat); + } + + if (isYearOnlyView(views as DatePickerView[])) { + return utils.format(date, 'year'); + } + + if (isYearAndMonthViews(views as DatePickerView[])) { + return utils.format(date, 'month'); + } + + // Little localization hack (Google is doing the same for android native pickers): + // For english localization it is convenient to include weekday into the date "Mon, Jun 1" + // For other locales using strings like "June 1", without weekday + return /en/.test(utils.getCurrentLocaleCode()) + ? utils.format(date, 'normalDateWithWeekday') + : utils.format(date, 'normalDate'); + }, [date, toolbarFormat, toolbarPlaceholder, utils, views]); + + return ( + + + {dateText} + + + ); +}; + +export default withStyles(styles, { name: 'MuiDatePickerToolbar' })(DatePickerToolbar); diff --git a/packages/material-ui-lab/src/DatePicker/index.ts b/packages/material-ui-lab/src/DatePicker/index.ts new file mode 100644 index 00000000000000..a4c99812fd6629 --- /dev/null +++ b/packages/material-ui-lab/src/DatePicker/index.ts @@ -0,0 +1,2 @@ +export { default } from './DatePicker'; +export * from './DatePicker'; diff --git a/packages/pickers/lib/src/DateRangePicker/DateRangeDelimiter.tsx b/packages/material-ui-lab/src/DateRangeDelimiter/DateRangeDelimiter.tsx similarity index 63% rename from packages/pickers/lib/src/DateRangePicker/DateRangeDelimiter.tsx rename to packages/material-ui-lab/src/DateRangeDelimiter/DateRangeDelimiter.tsx index cc0751e5ba96d5..cd3956b497b55c 100644 --- a/packages/pickers/lib/src/DateRangePicker/DateRangeDelimiter.tsx +++ b/packages/material-ui-lab/src/DateRangeDelimiter/DateRangeDelimiter.tsx @@ -1,12 +1,17 @@ import Typography from '@material-ui/core/Typography'; import { styled } from '@material-ui/core/styles'; -import { withDefaultProps } from '../_shared/withDefaultProps'; -export const DateRangeDelimiter = withDefaultProps( - { name: 'MuiPickersDateRangeDelimiter' }, - styled(Typography)({ +const DateRangeDelimiter = styled(Typography)( + { margin: '0 16px', - }) + }, + { name: 'MuiPickersDateRangeDelimiter' }, ); export type DateRangeDelimiterProps = React.ComponentProps; + +/** + * TODO use Box + * @ignore - internal component. + */ +export default DateRangeDelimiter; diff --git a/packages/material-ui-lab/src/DateRangeDelimiter/index.ts b/packages/material-ui-lab/src/DateRangeDelimiter/index.ts new file mode 100644 index 00000000000000..0b8fd34b98def9 --- /dev/null +++ b/packages/material-ui-lab/src/DateRangeDelimiter/index.ts @@ -0,0 +1,2 @@ +export * from './DateRangeDelimiter'; +export { default } from './DateRangeDelimiter'; diff --git a/packages/material-ui-lab/src/DateRangePicker/DateRangePicker.test.tsx b/packages/material-ui-lab/src/DateRangePicker/DateRangePicker.test.tsx new file mode 100644 index 00000000000000..3ed98b98f266b7 --- /dev/null +++ b/packages/material-ui-lab/src/DateRangePicker/DateRangePicker.test.tsx @@ -0,0 +1,289 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { isWeekend } from 'date-fns'; +import { screen, fireEvent } from 'test/utils'; +import TextField, { TextFieldProps } from '@material-ui/core/TextField'; +import DesktopDateRangePicker, { DateRange } from '@material-ui/lab/DesktopDateRangePicker'; +import StaticDateRangePicker from '@material-ui/lab/StaticDateRangePicker'; +import { + createPickerRender, + FakeTransitionComponent, + adapterToUse, + getAllByMuiTest, + queryByMuiTest, +} from '../internal/pickers/test-utils'; + +const defaultRangeRenderInput = (startProps: TextFieldProps, endProps: TextFieldProps) => ( + + + + +); + +describe('', () => { + const render = createPickerRender({ strict: false }); + + before(function beforeHook() { + if (!/jsdom/.test(window.navigator.userAgent)) { + // FIXME This test suite is extremely flaky in test:karma + this.skip(); + } + }); + + it('allows to select date range end-to-end', () => { + function RangePickerTest() { + const [range, changeRange] = React.useState>([ + new Date('2019-01-01T00:00:00.000'), + new Date('2019-01-01T00:00:00.000'), + ]); + + return ( + changeRange(date)} + value={range} + TransitionComponent={FakeTransitionComponent} + /> + ); + } + + render(); + + fireEvent.click(screen.getByLabelText('Jan 1, 2019')); + fireEvent.click(screen.getByLabelText('Jan 24, 2019')); + + expect(getAllByMuiTest('DateRangeHighlight')).to.have.length(24); + }); + + it('highlights the selected range of dates', () => { + render( + {}} + value={[ + adapterToUse.date(new Date('2018-01-01T00:00:00.000Z')), + adapterToUse.date(new Date('2018-01-31T00:00:00.000Z')), + ]} + />, + ); + + expect(getAllByMuiTest('DateRangeHighlight')).to.have.length(31); + }); + + it('selects the range from the next month', () => { + const onChangeMock = spy(); + render( + , + ); + + fireEvent.click(screen.getByLabelText('Jan 1, 2019')); + fireEvent.click( + screen.getByLabelText('next month', { selector: ':not([aria-hidden="true"])' }), + ); + fireEvent.click(screen.getByLabelText('Mar 19, 2019')); + + expect(onChangeMock.callCount).to.equal(2); + const [changedRange] = onChangeMock.lastCall.args; + expect(changedRange[0]).to.toEqualDateTime(new Date('2019-01-01T00:00:00.000')); + expect(changedRange[1]).to.toEqualDateTime(new Date('2019-03-19T00:00:00.000')); + }); + + it('continues start selection if selected "end" date is before start', () => { + const onChangeMock = spy(); + render( + , + ); + + fireEvent.click(screen.getByLabelText('Jan 30, 2019')); + fireEvent.click(screen.getByLabelText('Jan 19, 2019')); + + expect(queryByMuiTest(document.body, 'DateRangeHighlight')).to.equal(null); + + fireEvent.click(screen.getByLabelText('Jan 30, 2019')); + + expect(onChangeMock.callCount).to.equal(3); + const [changedRange] = onChangeMock.lastCall.args; + expect(changedRange[0]).to.toEqualDateTime(new Date('2019-01-19T00:00:00.000')); + expect(changedRange[1]).to.toEqualDateTime(new Date('2019-01-30T00:00:00.000')); + }); + + it('starts selection from end if end text field was focused', function test() { + const onChangeMock = spy(); + render( + , + ); + + fireEvent.focus(screen.getAllByRole('textbox')[1]); + + fireEvent.click(screen.getByLabelText('Jan 30, 2019')); + fireEvent.click(screen.getByLabelText('Jan 19, 2019')); + + expect(getAllByMuiTest('DateRangeHighlight')).to.have.length(12); + expect(onChangeMock.callCount).to.equal(2); + const [changedRange] = onChangeMock.lastCall.args; + expect(changedRange[0]).toEqualDateTime(new Date('2019-01-19T00:00:00.000')); + expect(changedRange[1]).toEqualDateTime(new Date('2019-01-30T00:00:00.000')); + }); + + it('closes on focus out of fields', () => { + render( + + {}} + TransitionComponent={FakeTransitionComponent} + /> + + , + ); + + fireEvent.focus(screen.getAllByRole('textbox')[0]); + expect(screen.getByRole('tooltip')).toBeVisible(); + + fireEvent.focus(screen.getByText('focus me')); + expect(screen.getByRole('tooltip')).not.toBeVisible(); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('allows pure keyboard selection of range', () => { + const onChangeMock = spy(); + render( + , + ); + + fireEvent.focus(screen.getAllByRole('textbox')[0]); + fireEvent.change(screen.getAllByRole('textbox')[0], { + target: { + value: '06/06/2019', + }, + }); + + fireEvent.change(screen.getAllByRole('textbox')[1], { + target: { + value: '08/08/2019', + }, + }); + + expect( + onChangeMock.calledWith([ + new Date('2019-06-06T00:00:00.000'), + new Date('2019-06-06T00:00:00.000'), + ]), + ).to.equal(true); + }); + + it('scrolls current month to the active selection on focusing appropriate field', () => { + render( + {}} + TransitionComponent={FakeTransitionComponent} + />, + ); + + fireEvent.focus(screen.getAllByRole('textbox')[0]); + expect(screen.getByText('May 2019')).toBeVisible(); + + fireEvent.focus(screen.getAllByRole('textbox')[1]); + expect(screen.getByText('October 2019')).toBeVisible(); + + // scroll back + fireEvent.focus(screen.getAllByRole('textbox')[0]); + expect(screen.getByText('May 2019')).toBeVisible(); + }); + + it('allows disabling dates', () => { + render( + {}} + value={[ + adapterToUse.date('2018-01-01T00:00:00.000'), + adapterToUse.date('2018-01-31T00:00:00.000'), + ]} + />, + ); + + expect( + getAllByMuiTest('DateRangeDay').filter((day) => day.getAttribute('disabled') !== undefined), + ).to.have.length(31); + }); + + it(`doesn't crash if opening picker with invalid date input`, async () => { + render( + {}} + TransitionComponent={FakeTransitionComponent} + value={[adapterToUse.date(new Date(NaN)), adapterToUse.date('2018-01-31T00:00:00.000')]} + />, + ); + + fireEvent.focus(screen.getAllByRole('textbox')[0]); + expect(screen.getByRole('tooltip')).toBeVisible(); + }); + + it('prop – `renderDay` should be called and render days', async () => { + render( + {}} + renderDay={(day) =>
} + value={[null, null]} + />, + ); + + expect(getAllByMuiTest('renderDayCalled')).not.to.have.length(0); + }); + + it('prop – `calendars` renders provided amount of calendars', () => { + render( + {}} + value={[ + adapterToUse.date('2018-01-01T00:00:00.000'), + adapterToUse.date('2018-01-31T00:00:00.000'), + ]} + />, + ); + + expect(getAllByMuiTest('pickers-calendar')).to.have.length(3); + }); +}); diff --git a/packages/material-ui-lab/src/DateRangePicker/DateRangePicker.tsx b/packages/material-ui-lab/src/DateRangePicker/DateRangePicker.tsx new file mode 100644 index 00000000000000..394bcf506b9550 --- /dev/null +++ b/packages/material-ui-lab/src/DateRangePicker/DateRangePicker.tsx @@ -0,0 +1,372 @@ +import PropTypes from 'prop-types'; +import { ResponsiveTooltipWrapper } from '../internal/pickers/wrappers/ResponsiveWrapper'; +import { makeDateRangePicker } from './makeDateRangePicker'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const DateRangePicker = makeDateRangePicker('MuiPickersDateRangePicker', ResponsiveTooltipWrapper); + +(DateRangePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * If `true`, `onChange` is fired on click even if the same date is selected. + * @default false + */ + allowSameDateSelection: PropTypes.bool, + /** + * The number of calendars that render on **desktop**. + * @default 2 + */ + calendars: PropTypes.oneOf([1, 2, 3]), + /** + * "CANCEL" Text message + * @default "CANCEL" + */ + cancelText: PropTypes.node, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * If `true`, it shows the clear action in the picker dialog. + * @default false + */ + clearable: PropTypes.bool, + /** + * "CLEAR" Text message + * @default "CLEAR" + */ + clearText: PropTypes.node, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * CSS media query when `Mobile` mode will be changed to `Desktop`. + * @default "@media (pointer: fine)" + * @example "@media (min-width: 720px)" or theme.breakpoints.up("sm") + */ + desktopModeMediaQuery: PropTypes.string, + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps: PropTypes.object, + /** + * if `true` after selecting `start` date calendar will not automatically switch to the month of `end` date + * @default false + */ + disableAutoMonthSwitching: PropTypes.bool, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * If `true`, todays date is rendering without highlighting with circle. + * @default false + */ + disableHighlightToday: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Text for end input label and toolbar placeholder. + * @default "end" + */ + endText: PropTypes.node, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * Get aria-label text for switching between views button. + */ + getViewSwitchingButtonText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * Max selectable date. @DateIOType + * @default Date(2099-31-12) + */ + maxDate: PropTypes.any, + /** + * Min selectable date. @DateIOType + * @default Date(1900-01-01) + */ + minDate: PropTypes.any, + /** + * "OK" button text. + * @default "OK" + */ + okText: PropTypes.node, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Popper props passed down to [Popper](https://material-ui.com/api/popper/) component. + */ + PopperProps: PropTypes.object, + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Custom renderer for `` days. @DateIOType + * @example (date, DateRangeDayProps) => + */ + renderDay: PropTypes.func, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `startProps` and `endProps` arguments of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api), + * that you need to forward to the range start/end inputs respectively. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example + * ```jsx + * ( + * + * + * to + * + * ; + * )} + * /> + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. + * @default false + */ + showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. + * @default false + */ + showTodayButton: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Text for start input label and toolbar placeholder. + * @default "Start" + */ + startText: PropTypes.node, + /** + * "TODAY" Text message + * @default "TODAY" + */ + todayText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * Custom component for popper [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). + */ + TransitionComponent: PropTypes.elementType, + /** + * The value of the picker. + */ + value: PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + ).isRequired, +}; + +export type DateRangePickerProps = React.ComponentProps; + +export type DateRange = import('./RangeTypes').DateRange; + +export default DateRangePicker; diff --git a/packages/pickers/lib/src/DateRangePicker/DateRangePickerInput.tsx b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerInput.tsx similarity index 78% rename from packages/pickers/lib/src/DateRangePicker/DateRangePickerInput.tsx rename to packages/material-ui-lab/src/DateRangePicker/DateRangePickerInput.tsx index bf19a1fef51f06..ee8b2469c9ab3b 100644 --- a/packages/pickers/lib/src/DateRangePicker/DateRangePickerInput.tsx +++ b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerInput.tsx @@ -1,16 +1,15 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; -import { useUtils } from '../_shared/hooks/useUtils'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; import { RangeInput, DateRange, CurrentlySelectingRangeEndProps } from './RangeTypes'; -import { useMaskedInput } from '../_shared/hooks/useMaskedInput'; -import { DateRangeValidationError } from '../_helpers/date-utils'; -import { WrapperVariantContext } from '../wrappers/WrapperVariantContext'; -import { mergeRefs, executeInTheNextEventLoopTick } from '../_helpers/utils'; -import { DateInputProps, MuiTextFieldProps } from '../_shared/PureDateInput'; +import { useMaskedInput } from '../internal/pickers/hooks/useMaskedInput'; +import { DateRangeValidationError } from '../internal/pickers/date-utils'; +import { WrapperVariantContext } from '../internal/pickers/wrappers/WrapperVariantContext'; +import { mergeRefs, executeInTheNextEventLoopTick } from '../internal/pickers/utils'; +import { DateInputProps, MuiTextFieldProps } from '../internal/pickers/PureDateInput'; -export const useStyles = makeStyles( - (theme) => ({ +export const styles = (theme: Theme) => + createStyles({ root: { display: 'flex', alignItems: 'baseline', @@ -25,9 +24,7 @@ export const useStyles = makeStyles( margin: '0 16px', }, }, - }), - { name: 'MuiPickersDateRangePickerInput' } -); + }); export interface ExportedDateRangePickerInputProps { /** @@ -38,14 +35,14 @@ export interface ExportedDateRangePickerInputProps { * @example * ```jsx * ( - - - to - - ; - )} - /> + * renderInput={(startProps, endProps) => ( + * + * + * to + * + * ; + * )} + * /> * ```` */ renderInput: (startProps: MuiTextFieldProps, endProps: MuiTextFieldProps) => React.ReactElement; @@ -65,7 +62,11 @@ export interface DateRangeInputProps validationError: DateRangeValidationError; } -export const DateRangePickerInput: React.FC = ({ +/** + * @ignore - internal component. + */ +const DateRangePickerInput: React.FC> = ({ + classes, containerRef, currentlySelectingRangeEnd, disableOpenPicker, @@ -86,7 +87,6 @@ export const DateRangePickerInput: React.FC = ({ ...other }) => { const utils = useUtils(); - const classes = useStyles(); const startRef = React.useRef(null); const endRef = React.useRef(null); const wrapperVariant = React.useContext(WrapperVariantContext); @@ -108,7 +108,7 @@ export const DateRangePickerInput: React.FC = ({ const lazyHandleChangeCallback = React.useCallback( (...args: Parameters) => executeInTheNextEventLoopTick(() => onChange(...args)), - [onChange] + [onChange], ); const handleStartChange = (date: unknown, inputString?: string) => { @@ -183,12 +183,4 @@ export const DateRangePickerInput: React.FC = ({ ); }; -DateRangePickerInput.propTypes = { - acceptRegex: PropTypes.instanceOf(RegExp), - getOpenDialogAriaText: PropTypes.func, - mask: PropTypes.string, - OpenPickerButtonProps: PropTypes.object, - openPickerIcon: PropTypes.node, - renderInput: PropTypes.func.isRequired, - rifmFormatter: PropTypes.func, -}; +export default withStyles(styles, { name: 'MuiPickersDateRangePickerInput' })(DateRangePickerInput); diff --git a/packages/material-ui-lab/src/DateRangePicker/DateRangePickerToolbar.tsx b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerToolbar.tsx new file mode 100644 index 00000000000000..d6a818dba61378 --- /dev/null +++ b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerToolbar.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import Typography from '@material-ui/core/Typography'; +import { withStyles, createStyles, WithStyles } from '@material-ui/core/styles'; +import PickersToolbar from '../internal/pickers/PickersToolbar'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import PickersToolbarButton from '../internal/pickers/PickersToolbarButton'; +import { ToolbarComponentProps } from '../internal/pickers/typings/BasePicker'; +import { DateRange, CurrentlySelectingRangeEndProps } from './RangeTypes'; + +export const styles = createStyles({ + root: {}, + penIcon: { + position: 'relative', + top: 4, + }, + dateTextContainer: { + display: 'flex', + }, +}); + +interface DateRangePickerToolbarProps + extends CurrentlySelectingRangeEndProps, + Pick< + ToolbarComponentProps, + 'isMobileKeyboardViewOpen' | 'toggleMobileKeyboardView' | 'toolbarTitle' | 'toolbarFormat' + > { + date: DateRange; + startText: React.ReactNode; + endText: React.ReactNode; + currentlySelectingRangeEnd: 'start' | 'end'; + setCurrentlySelectingRangeEnd: (newSelectingEnd: 'start' | 'end') => void; +} + +/** + * @ignore - internal component. + */ +const DateRangePickerToolbar: React.FC> = ({ + classes, + currentlySelectingRangeEnd, + date: [start, end], + endText, + isMobileKeyboardViewOpen, + setCurrentlySelectingRangeEnd, + startText, + toggleMobileKeyboardView, + toolbarFormat, + toolbarTitle = 'SELECT DATE RANGE', +}) => { + const utils = useUtils(); + + const startDateValue = start + ? utils.formatByString(start, toolbarFormat || utils.formats.shortDate) + : startText; + + const endDateValue = end + ? utils.formatByString(end, toolbarFormat || utils.formats.shortDate) + : endText; + + return ( + +
+ setCurrentlySelectingRangeEnd('start')} + /> +  {'–'}  + setCurrentlySelectingRangeEnd('end')} + /> +
+
+ ); +}; + +export default withStyles(styles, { name: 'MuiPickersDateRangePickerToolbarProps' })( + DateRangePickerToolbar, +); diff --git a/packages/pickers/lib/src/DateRangePicker/DateRangePickerView.tsx b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerView.tsx similarity index 81% rename from packages/pickers/lib/src/DateRangePicker/DateRangePickerView.tsx rename to packages/material-ui-lab/src/DateRangePicker/DateRangePickerView.tsx index 83193362dffbd7..2a73bedbee0c3a 100644 --- a/packages/pickers/lib/src/DateRangePicker/DateRangePickerView.tsx +++ b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerView.tsx @@ -1,27 +1,24 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; -import { isRangeValid } from '../_helpers/date-utils'; -import { BasePickerProps } from '../typings/BasePicker'; +import PropTypes from 'prop-types'; +import { isRangeValid } from '../internal/pickers/date-utils'; +import { BasePickerProps } from '../internal/pickers/typings/BasePicker'; import { calculateRangeChange } from './date-range-manager'; -import { useUtils, useNow } from '../_shared/hooks/useUtils'; -import { SharedPickerProps } from '../Picker/SharedPickerProps'; -import { DateRangePickerToolbar } from './DateRangePickerToolbar'; -import { useParsedDate } from '../_shared/hooks/date-helpers-hooks'; -import { useCalendarState } from '../views/Calendar/useCalendarState'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import { SharedPickerProps } from '../internal/pickers/Picker/SharedPickerProps'; +import DateRangePickerToolbar from './DateRangePickerToolbar'; +import { useCalendarState } from '../DayPicker/useCalendarState'; import { DateRangePickerViewMobile } from './DateRangePickerViewMobile'; -import { defaultMaxDate, defaultMinDate } from '../constants/prop-types'; -import { WrapperVariantContext } from '../wrappers/WrapperVariantContext'; -import { MobileKeyboardInputView } from '../views/MobileKeyboardInputView'; -import { DateRangePickerInput, DateRangeInputProps } from './DateRangePickerInput'; +import { WrapperVariantContext } from '../internal/pickers/wrappers/WrapperVariantContext'; +import { MobileKeyboardInputView } from '../internal/pickers/Picker/Picker'; +import DateRangePickerInput, { DateRangeInputProps } from './DateRangePickerInput'; import { RangeInput, DateRange, CurrentlySelectingRangeEndProps } from './RangeTypes'; -import { ExportedCalendarViewProps, defaultReduceAnimations } from '../views/Calendar/CalendarView'; -import { - DateRangePickerViewDesktop, +import { ExportedDayPickerProps, defaultReduceAnimations } from '../DayPicker/DayPicker'; +import DateRangePickerViewDesktop, { ExportedDesktopDateRangeCalendarProps, } from './DateRangePickerViewDesktop'; type BaseCalendarPropsToReuse = Omit< - ExportedCalendarViewProps, + ExportedDayPickerProps, 'onYearChange' | 'renderDay' >; @@ -31,7 +28,6 @@ export interface ExportedDateRangePickerViewProps Omit { /** * if `true` after selecting `start` date calendar will not automatically switch to the month of `end` date - * * @default false */ disableAutoMonthSwitching?: boolean; @@ -46,6 +42,9 @@ interface DateRangePickerViewProps endText: React.ReactNode; } +/** + * @ignore - internal component. + */ export function DateRangePickerView(props: DateRangePickerViewProps) { const { calendars = 2, @@ -53,14 +52,15 @@ export function DateRangePickerView(props: DateRangePickerViewProps(props: DateRangePickerViewProps(); const utils = useUtils(); const wrapperVariant = React.useContext(WrapperVariantContext); - const minDate = useParsedDate(unparsedMinDate) as TDate; - const maxDate = useParsedDate(unparsedMaxDate) as TDate; const [start, end] = date; const { @@ -89,15 +86,16 @@ export function DateRangePickerView(props: DateRangePickerViewProps(props: DateRangePickerViewProps, wrapperVariant, - isFullRangeSelected ? 'finish' : 'partial' + isFullRangeSelected ? 'finish' : 'partial', ); }, [ @@ -167,7 +165,7 @@ export function DateRangePickerView(props: DateRangePickerViewProps { diff --git a/packages/pickers/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerViewDesktop.tsx similarity index 78% rename from packages/pickers/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx rename to packages/material-ui-lab/src/DateRangePicker/DateRangePickerViewDesktop.tsx index d2a3a6e79d3b38..62d31fcd5887ac 100644 --- a/packages/pickers/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx +++ b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerViewDesktop.tsx @@ -1,27 +1,29 @@ import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; import { DateRange } from './RangeTypes'; -import { useUtils } from '../_shared/hooks/useUtils'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; import { calculateRangePreview } from './date-range-manager'; -import { Calendar, CalendarProps } from '../views/Calendar/Calendar'; -import { DateRangeDay, DateRangeDayProps } from './DateRangePickerDay'; -import { defaultMinDate, defaultMaxDate } from '../constants/prop-types'; -import { ArrowSwitcher, ExportedArrowSwitcherProps } from '../_shared/ArrowSwitcher'; +import PickersCalendar, { PickersCalendarProps } from '../DayPicker/PickersCalendar'; +import DateRangeDay, { DateRangePickerDayProps } from '../DateRangePickerDay'; +import { defaultMinDate, defaultMaxDate } from '../internal/pickers/constants/prop-types'; +import ArrowSwitcher, { + ExportedArrowSwitcherProps, +} from '../internal/pickers/PickersArrowSwitcher'; import { usePreviousMonthDisabled, useNextMonthDisabled, -} from '../_shared/hooks/date-helpers-hooks'; +} from '../internal/pickers/hooks/date-helpers-hooks'; import { isWithinRange, isStartOfRange, isEndOfRange, DateValidationProps, -} from '../_helpers/date-utils'; +} from '../internal/pickers/date-utils'; +import { doNothing } from '../internal/pickers/utils'; export interface ExportedDesktopDateRangeCalendarProps { /** - * How many calendars render on **desktop** DateRangePicker. - * + * The number of calendars that render on **desktop**. * @default 2 */ calendars?: 1 | 2 | 3; @@ -29,12 +31,12 @@ export interface ExportedDesktopDateRangeCalendarProps { * Custom renderer for `` days. @DateIOType * @example (date, DateRangeDayProps) => */ - renderDay?: (date: TDate, DateRangeDayProps: DateRangeDayProps) => JSX.Element; + renderDay?: (date: TDate, DateRangeDayProps: DateRangePickerDayProps) => JSX.Element; } interface DesktopDateRangeCalendarProps extends ExportedDesktopDateRangeCalendarProps, - Omit, 'renderDay'>, + Omit, 'renderDay' | 'onFocusedDayChange'>, DateValidationProps, ExportedArrowSwitcherProps { date: DateRange; @@ -42,8 +44,8 @@ interface DesktopDateRangeCalendarProps currentlySelectingRangeEnd: 'start' | 'end'; } -export const useStyles = makeStyles( - (theme) => ({ +export const styles = (theme: Theme) => + createStyles({ root: { display: 'flex', flexDirection: 'row', @@ -63,9 +65,7 @@ export const useStyles = makeStyles( alignItems: 'center', justifyContent: 'space-between', }, - }), - { name: 'MuiPickersDesktopDateRangeCalendar' } -); + }); function getCalendarsArray(calendars: ExportedDesktopDateRangeCalendarProps['calendars']) { switch (calendars) { @@ -81,16 +81,22 @@ function getCalendarsArray(calendars: ExportedDesktopDateRangeCalendarProps(props: DesktopDateRangeCalendarProps) { +/** + * @ignore - internal component. + */ +function DateRangePickerViewDesktop( + props: DesktopDateRangeCalendarProps & WithStyles, +) { const { date, + classes, calendars = 2, changeMonth, leftArrowButtonProps, - leftArrowButtonText, + leftArrowButtonText = 'previous month', leftArrowIcon, rightArrowButtonProps, - rightArrowButtonText, + rightArrowButtonText = 'next month', rightArrowIcon, onChange, disableFuture, @@ -106,7 +112,6 @@ export function DateRangePickerViewDesktop(props: DesktopDateRangeCalenda } = props; const utils = useUtils(); - const classes = useStyles(); const minDate = __minDate || utils.date(defaultMinDate); const maxDate = __maxDate || utils.date(defaultMaxDate); @@ -127,7 +132,7 @@ export function DateRangePickerViewDesktop(props: DesktopDateRangeCalenda setRangePreviewDay(null); onChange(day); }, - [onChange] + [onChange], ); const handlePreviewDayChange = (newPreviewRequest: TDate) => { @@ -142,7 +147,7 @@ export function DateRangePickerViewDesktop(props: DesktopDateRangeCalenda () => ({ onMouseLeave: () => setRangePreviewDay(null), }), - [] + [], ); const selectNextMonth = React.useCallback(() => { @@ -176,10 +181,11 @@ export function DateRangePickerViewDesktop(props: DesktopDateRangeCalenda rightArrowIcon={rightArrowIcon} text={utils.format(monthOnIteration, 'monthAndYear')} /> - + {...other} key={index} date={date} + onFocusedDayChange={doNothing} className={classes.calendar} onChange={handleDayChange} currentMonth={monthOnIteration} @@ -203,3 +209,7 @@ export function DateRangePickerViewDesktop(props: DesktopDateRangeCalenda
); } + +export default withStyles(styles, { name: 'MuiPickersDesktopDateRangeCalendar' })( + DateRangePickerViewDesktop, +) as (props: DesktopDateRangeCalendarProps) => JSX.Element; diff --git a/packages/pickers/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerViewMobile.tsx similarity index 73% rename from packages/pickers/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx rename to packages/material-ui-lab/src/DateRangePicker/DateRangePickerViewMobile.tsx index 9f48ae359bb36c..7badee5233275a 100644 --- a/packages/pickers/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx +++ b/packages/material-ui-lab/src/DateRangePicker/DateRangePickerViewMobile.tsx @@ -1,24 +1,27 @@ import * as React from 'react'; -import { CalendarHeader, ExportedCalendarHeaderProps } from '../views/Calendar/CalendarHeader'; +import PickersCalendarHeader, { + ExportedCalendarHeaderProps, +} from '../DayPicker/PickersCalendarHeader'; import { DateRange } from './RangeTypes'; -import { DateRangeDay } from './DateRangePickerDay'; -import { useUtils } from '../_shared/hooks/useUtils'; -import { Calendar, CalendarProps } from '../views/Calendar/Calendar'; -import { defaultMinDate, defaultMaxDate } from '../constants/prop-types'; +import DateRangeDay from '../DateRangePickerDay/DateRangePickerDay'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import PickersCalendar, { PickersCalendarProps } from '../DayPicker/PickersCalendar'; +import { defaultMinDate, defaultMaxDate } from '../internal/pickers/constants/prop-types'; import { ExportedDesktopDateRangeCalendarProps } from './DateRangePickerViewDesktop'; import { isWithinRange, isStartOfRange, isEndOfRange, DateValidationProps, -} from '../_helpers/date-utils'; +} from '../internal/pickers/date-utils'; +import { doNothing } from '../internal/pickers/utils'; export interface ExportedMobileDateRangeCalendarProps extends Pick, 'renderDay'> {} interface DesktopDateRangeCalendarProps extends ExportedMobileDateRangeCalendarProps, - Omit, 'date' | 'renderDay'>, + Omit, 'date' | 'renderDay' | 'onFocusedDayChange'>, DateValidationProps, ExportedCalendarHeaderProps { date: DateRange; @@ -27,6 +30,9 @@ interface DesktopDateRangeCalendarProps const onlyDateView = ['date'] as ['date']; +/** + * @ignore - internal component. + */ export function DateRangePickerViewMobile(props: DesktopDateRangeCalendarProps) { const { changeMonth, @@ -42,7 +48,7 @@ export function DateRangePickerViewMobile(props: DesktopDateRangeCalendar rightArrowButtonProps, rightArrowButtonText, rightArrowIcon, - renderDay = (_, props) => {...props} />, + renderDay = (_, dayProps) => {...dayProps} />, ...other } = props; @@ -52,10 +58,9 @@ export function DateRangePickerViewMobile(props: DesktopDateRangeCalendar return ( - ({})} onMonthChange={changeMonth as any} leftArrowButtonText={leftArrowButtonText} leftArrowButtonProps={leftArrowButtonProps} @@ -67,10 +72,11 @@ export function DateRangePickerViewMobile(props: DesktopDateRangeCalendar maxDate={maxDate} {...other} /> - + {...other} date={date} onChange={onChange} + onFocusedDayChange={doNothing} renderDay={(day, _, DayProps) => renderDay(day, { isPreviewing: false, diff --git a/packages/material-ui-lab/src/DateRangePicker/RangeTypes.ts b/packages/material-ui-lab/src/DateRangePicker/RangeTypes.ts new file mode 100644 index 00000000000000..f2fdcefe4029a7 --- /dev/null +++ b/packages/material-ui-lab/src/DateRangePicker/RangeTypes.ts @@ -0,0 +1,17 @@ +import { ParsableDate } from '../internal/pickers/constants/prop-types'; +import { AllSharedPickerProps } from '../internal/pickers/Picker/SharedPickerProps'; + +export type RangeInput = [ParsableDate, ParsableDate]; +export type DateRange = [TDate | null, TDate | null]; +export type NonEmptyDateRange = [TDate, TDate]; + +export type AllSharedDateRangePickerProps = Omit< + AllSharedPickerProps, DateRange>, + 'renderInput' | 'orientation' +> & + import('./DateRangePickerInput').ExportedDateRangePickerInputProps; + +export interface CurrentlySelectingRangeEndProps { + currentlySelectingRangeEnd: 'start' | 'end'; + setCurrentlySelectingRangeEnd: (newSelectingEnd: 'start' | 'end') => void; +} diff --git a/packages/material-ui-lab/src/DateRangePicker/date-range-manager.test.ts b/packages/material-ui-lab/src/DateRangePicker/date-range-manager.test.ts new file mode 100644 index 00000000000000..03381be898c7af --- /dev/null +++ b/packages/material-ui-lab/src/DateRangePicker/date-range-manager.test.ts @@ -0,0 +1,152 @@ +import { expect } from 'chai'; +import { calculateRangeChange, calculateRangePreview } from './date-range-manager'; +import { adapterToUse } from '../internal/pickers/test-utils'; +import { DateRange } from './RangeTypes'; + +const start2018 = new Date('2018-01-01T00:00:00.000Z'); +const mid2018 = new Date('2018-06-01T00:00:00.000Z'); +const end2019 = new Date('2019-01-01T00:00:00.000Z'); + +describe('date-range-manager', () => { + [ + { + range: [null, null], + selectingEnd: 'start' as const, + newDate: start2018, + expectedRange: [start2018, null], + expectedNextSelection: 'end' as const, + }, + { + range: [start2018, null], + selectingEnd: 'start' as const, + newDate: end2019, + expectedRange: [end2019, null], + expectedNextSelection: 'end' as const, + }, + { + range: [null, end2019], + selectingEnd: 'start' as const, + newDate: mid2018, + expectedRange: [mid2018, end2019], + expectedNextSelection: 'end' as const, + }, + { + range: [null, end2019], + selectingEnd: 'end' as const, + newDate: mid2018, + expectedRange: [null, mid2018], + expectedNextSelection: 'start' as const, + }, + { + range: [mid2018, null], + selectingEnd: 'start' as const, + newDate: start2018, + expectedRange: [start2018, null], + expectedNextSelection: 'end' as const, + }, + { + range: [start2018, end2019], + selectingEnd: 'start' as const, + newDate: mid2018, + expectedRange: [mid2018, end2019], + expectedNextSelection: 'end' as const, + }, + { + range: [start2018, end2019], + selectingEnd: 'end' as const, + newDate: mid2018, + expectedRange: [start2018, mid2018], + expectedNextSelection: 'start' as const, + }, + { + range: [mid2018, end2019], + selectingEnd: 'start' as const, + newDate: start2018, + expectedRange: [start2018, end2019], + expectedNextSelection: 'end' as const, + }, + { + range: [start2018, mid2018], + selectingEnd: 'end' as const, + newDate: mid2018, + expectedRange: [start2018, mid2018], + expectedNextSelection: 'start' as const, + }, + ].forEach(({ range, selectingEnd, newDate, expectedRange, expectedNextSelection }) => { + it(`calculateRangeChange should return ${expectedRange} when selecting ${selectingEnd} of ${range} with user input ${newDate}`, () => { + expect( + calculateRangeChange({ + utils: adapterToUse, + range: range as DateRange, + newDate, + currentlySelectingRangeEnd: selectingEnd, + }), + ).to.deep.equal({ + nextSelection: expectedNextSelection, + newRange: expectedRange, + }); + }); + }); + + [ + { + range: [start2018, end2019], + selectingEnd: 'start' as const, + newDate: null, + expectedRangePreview: [null, null], + }, + { + range: [null, null], + selectingEnd: 'start' as const, + newDate: start2018, + expectedRangePreview: [start2018, null], + }, + { + range: [start2018, null], + selectingEnd: 'start' as const, + newDate: end2019, + expectedRangePreview: [end2019, null], + }, + { + range: [null, end2019], + selectingEnd: 'start' as const, + newDate: mid2018, + expectedRangePreview: [mid2018, end2019], + }, + { + range: [null, end2019], + selectingEnd: 'end' as const, + newDate: mid2018, + expectedRangePreview: [null, mid2018], + }, + { + range: [mid2018, null], + selectingEnd: 'start' as const, + newDate: start2018, + expectedRangePreview: [start2018, null], + }, + { + range: [mid2018, end2019], + selectingEnd: 'start' as const, + newDate: start2018, + expectedRangePreview: [start2018, mid2018], + }, + { + range: [start2018, mid2018], + selectingEnd: 'end' as const, + newDate: end2019, + expectedRangePreview: [mid2018, end2019], + }, + ].forEach(({ range, selectingEnd, newDate, expectedRangePreview }) => { + it(`calculateRangePreview should return ${expectedRangePreview} when selecting ${selectingEnd} of $range when user hover ${newDate}`, () => { + expect( + calculateRangePreview({ + utils: adapterToUse, + range: range as DateRange, + newDate, + currentlySelectingRangeEnd: selectingEnd, + }), + ).to.deep.equal(expectedRangePreview); + }); + }); +}); diff --git a/packages/pickers/lib/src/DateRangePicker/date-range-manager.ts b/packages/material-ui-lab/src/DateRangePicker/date-range-manager.ts similarity index 59% rename from packages/pickers/lib/src/DateRangePicker/date-range-manager.ts rename to packages/material-ui-lab/src/DateRangePicker/date-range-manager.ts index 619d19cee51ee0..408deb9d2b1e67 100644 --- a/packages/pickers/lib/src/DateRangePicker/date-range-manager.ts +++ b/packages/material-ui-lab/src/DateRangePicker/date-range-manager.ts @@ -1,33 +1,38 @@ import { DateRange } from './RangeTypes'; -import { MuiPickersAdapter } from '../_shared/hooks/useUtils'; +import { MuiPickersAdapter } from '../internal/pickers/hooks/useUtils'; -interface CalculateRangeChangeOptions { - utils: MuiPickersAdapter; - range: DateRange; - newDate: unknown; +interface CalculateRangeChangeOptions { + utils: MuiPickersAdapter; + range: DateRange; + newDate: TDate; currentlySelectingRangeEnd: 'start' | 'end'; } -export function calculateRangeChange({ +export function calculateRangeChange({ utils, range, newDate: selectedDate, currentlySelectingRangeEnd, -}: CalculateRangeChangeOptions): { nextSelection: 'start' | 'end'; newRange: DateRange } { +}: CalculateRangeChangeOptions): { + nextSelection: 'start' | 'end'; + newRange: DateRange; +} { const [start, end] = range; if (currentlySelectingRangeEnd === 'start') { - return Boolean(end) && utils.isAfter(selectedDate, end) + return Boolean(end) && utils.isAfter(selectedDate, end!) ? { nextSelection: 'end', newRange: [selectedDate, null] } : { nextSelection: 'end', newRange: [selectedDate, end] }; } - return Boolean(start) && utils.isBefore(selectedDate, start) + return Boolean(start) && utils.isBefore(selectedDate, start!) ? { nextSelection: 'end', newRange: [selectedDate, null] } : { nextSelection: 'start', newRange: [start, selectedDate] }; } -export function calculateRangePreview(options: CalculateRangeChangeOptions): DateRange { +export function calculateRangePreview( + options: CalculateRangeChangeOptions, +): DateRange { if (!options.newDate) { return [null, null]; } diff --git a/packages/material-ui-lab/src/DateRangePicker/index.ts b/packages/material-ui-lab/src/DateRangePicker/index.ts new file mode 100644 index 00000000000000..f779bc448d6658 --- /dev/null +++ b/packages/material-ui-lab/src/DateRangePicker/index.ts @@ -0,0 +1,2 @@ +export * from './DateRangePicker'; +export { default } from './DateRangePicker'; diff --git a/packages/pickers/lib/src/DateRangePicker/DateRangePicker.tsx b/packages/material-ui-lab/src/DateRangePicker/makeDateRangePicker.tsx similarity index 55% rename from packages/pickers/lib/src/DateRangePicker/DateRangePicker.tsx rename to packages/material-ui-lab/src/DateRangePicker/makeDateRangePicker.tsx index 346097e83dc851..d66994dfd54242 100644 --- a/packages/pickers/lib/src/DateRangePicker/DateRangePicker.tsx +++ b/packages/material-ui-lab/src/DateRangePicker/makeDateRangePicker.tsx @@ -1,71 +1,62 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; -import { useUtils } from '../_shared/hooks/useUtils'; -import { MobileWrapper } from '../wrappers/MobileWrapper'; -import { withDefaultProps } from '../_shared/withDefaultProps'; -import { useParsedDate } from '../_shared/hooks/date-helpers-hooks'; -import { withDateAdapterProp } from '../_shared/withDateAdapterProp'; -import { makeWrapperComponent } from '../wrappers/makeWrapperComponent'; -import { ResponsiveTooltipWrapper } from '../wrappers/ResponsiveWrapper'; -import { defaultMinDate, defaultMaxDate, date } from '../constants/prop-types'; -import { DesktopTooltipWrapper } from '../wrappers/DesktopTooltipWrapper'; -import { SomeWrapper, ExtendWrapper, StaticWrapper } from '../wrappers/Wrapper'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import { withDefaultProps } from '../internal/pickers/withDefaultProps'; +import { useParsedDate } from '../internal/pickers/hooks/date-helpers-hooks'; +import { withDateAdapterProp } from '../internal/pickers/withDateAdapterProp'; +import { makeWrapperComponent } from '../internal/pickers/wrappers/makeWrapperComponent'; +import { defaultMinDate, defaultMaxDate } from '../internal/pickers/constants/prop-types'; +import { SomeWrapper, ExtendWrapper } from '../internal/pickers/wrappers/Wrapper'; import { RangeInput, AllSharedDateRangePickerProps, DateRange } from './RangeTypes'; -import { makeValidationHook, ValidationProps } from '../_shared/hooks/useValidation'; -import { usePickerState, PickerStateValueManager } from '../_shared/hooks/usePickerState'; +import { makeValidationHook, ValidationProps } from '../internal/pickers/hooks/useValidation'; +import { usePickerState, PickerStateValueManager } from '../internal/pickers/hooks/usePickerState'; import { DateRangePickerView, ExportedDateRangePickerViewProps } from './DateRangePickerView'; -import { - DateRangePickerInput, - ExportedDateRangePickerInputProps, - DateRangeInputProps, -} from './DateRangePickerInput'; +import DateRangePickerInput, { ExportedDateRangePickerInputProps } from './DateRangePickerInput'; import { parseRangeInputValue, validateDateRange, DateRangeValidationError, -} from '../_helpers/date-utils'; +} from '../internal/pickers/date-utils'; +import { DateInputPropsLike } from '../internal/pickers/wrappers/WrapperProps'; export interface BaseDateRangePickerProps extends ExportedDateRangePickerViewProps, ValidationProps>, ExportedDateRangePickerInputProps { /** - * Text for start input label and toolbar placeholder - * + * Text for start input label and toolbar placeholder. * @default "Start" */ startText?: React.ReactNode; /** - * Text for end input label and toolbar placeholder - * + * Text for end input label and toolbar placeholder. * @default "end" */ endText?: React.ReactNode; } -type RangePickerComponent = ( +export type DateRangePickerComponent = ( props: BaseDateRangePickerProps & ExtendWrapper & AllSharedDateRangePickerProps & - React.RefAttributes + React.RefAttributes, ) => JSX.Element; export const useDateRangeValidation = makeValidationHook< DateRangeValidationError, - RangeInput, + RangeInput, BaseDateRangePickerProps >(validateDateRange, { defaultValidationError: [null, null], isSameError: (a, b) => a[1] === b[1] && a[0] === b[0], }); -export function makeRangePicker( +export function makeDateRangePicker( name: string, - Wrapper: TWrapper -): RangePickerComponent { - const WrapperComponent = makeWrapperComponent(Wrapper, { - KeyboardDateInputComponent: DateRangePickerInput, - PureDateInputComponent: DateRangePickerInput, + Wrapper: TWrapper, +): DateRangePickerComponent { + const WrapperComponent = makeWrapperComponent(Wrapper, { + KeyboardDateInputComponent: DateRangePickerInput as React.FC, + PureDateInputComponent: DateRangePickerInput as React.FC, }); const rangePickerValueManager: PickerStateValueManager = { @@ -143,45 +134,14 @@ export function makeRangePicker( ); } - RangePickerWithStateAndWrapper.propTypes = { - value: PropTypes.arrayOf(date).isRequired, - onChange: PropTypes.func.isRequired, - startText: PropTypes.node, - endText: PropTypes.node, - } as any; - const FinalPickerComponent = withDefaultProps( { name }, - withDateAdapterProp(RangePickerWithStateAndWrapper) + withDateAdapterProp(RangePickerWithStateAndWrapper), ); - // @ts-ignore @see lib/src/Picker/makePickerWithState.tsx:95 + // @ts-expect-error Impossible to save component generics when wrapping with HOC return React.forwardRef< HTMLDivElement, React.ComponentProps >((props, ref) => ); } - -export const DateRangePicker = makeRangePicker( - 'MuiPickersDateRangePicker', - ResponsiveTooltipWrapper -); - -export type DateRangePickerProps = React.ComponentProps; - -export const DesktopDateRangePicker = makeRangePicker( - 'MuiDesktopDateRangePicker', - DesktopTooltipWrapper -); - -export type DesktopDateRangePickerProps = React.ComponentProps; - -export const MobileDateRangePicker = makeRangePicker('MuiMobileDateRangePicker', MobileWrapper); - -export type MobileDateRangePickerProps = React.ComponentProps; - -export const StaticDateRangePicker = makeRangePicker('MuiStaticDateRangePicker', StaticWrapper); - -export type StaticDateRangePickerProps = React.ComponentProps; - -export { DateRangeDelimiter } from './DateRangeDelimiter'; diff --git a/packages/material-ui-lab/src/DateRangePickerDay/DateRangePickerDay.test.tsx b/packages/material-ui-lab/src/DateRangePickerDay/DateRangePickerDay.test.tsx new file mode 100644 index 00000000000000..1e18167eb23575 --- /dev/null +++ b/packages/material-ui-lab/src/DateRangePickerDay/DateRangePickerDay.test.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { getClasses, createMount, describeConformance } from 'test/utils'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import DateRangePickerDay from '@material-ui/lab/DateRangePickerDay'; + +describe('', () => { + const mount = createMount(); + let classes: Record; + + const localizedMount = (node: React.ReactNode) => { + return mount({node}); + }; + + before(() => { + classes = getClasses( + {}} + isHighlighting + isPreviewing + isStartOfPreviewing + isEndOfPreviewing + isStartOfHighlighting + isEndOfHighlighting + />, + ); + }); + + describeConformance( + {}} + isHighlighting + isPreviewing + isStartOfPreviewing + isEndOfPreviewing + isStartOfHighlighting + isEndOfHighlighting + />, + () => ({ + classes, + inheritComponent: 'button', + mount: localizedMount, + refInstanceof: window.HTMLButtonElement, + // cannot test reactTestRenderer because of required context + skip: ['componentProp', 'reactTestRenderer', 'propsSpread', 'refForwarding'], + }), + ); +}); diff --git a/packages/pickers/lib/src/DateRangePicker/DateRangePickerDay.tsx b/packages/material-ui-lab/src/DateRangePickerDay/DateRangePickerDay.tsx similarity index 50% rename from packages/pickers/lib/src/DateRangePicker/DateRangePickerDay.tsx rename to packages/material-ui-lab/src/DateRangePickerDay/DateRangePickerDay.tsx index f3e3363acfed41..19351c3970efae 100644 --- a/packages/pickers/lib/src/DateRangePicker/DateRangePickerDay.tsx +++ b/packages/material-ui-lab/src/DateRangePickerDay/DateRangePickerDay.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { makeStyles, fade } from '@material-ui/core/styles'; -import { DAY_MARGIN } from '../constants/dimensions'; -import { useUtils } from '../_shared/hooks/useUtils'; -import { Day, DayProps, areDayPropsEqual } from '../views/Calendar/Day'; +import { withStyles, WithStyles, alpha, createStyles, Theme } from '@material-ui/core/styles'; +import { DAY_MARGIN } from '../internal/pickers/constants/dimensions'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import PickersDay, { PickersDayProps, areDayPropsEqual } from '../PickersDay/PickersDay'; -export interface DateRangeDayProps extends DayProps { +export interface DateRangePickerDayProps extends PickersDayProps { isHighlighting: boolean; isEndOfHighlighting: boolean; isStartOfHighlighting: boolean; @@ -24,8 +25,8 @@ const startBorderStyle = { borderBottomLeftRadius: '50%', }; -const useStyles = makeStyles( - (theme) => ({ +const styles = (theme: Theme) => + createStyles({ root: { '&:first-child $rangeIntervalDayPreview': { ...startBorderStyle, @@ -39,7 +40,7 @@ const useStyles = makeStyles( rangeIntervalDayHighlight: { borderRadius: 0, color: theme.palette.primary.contrastText, - backgroundColor: fade(theme.palette.primary.light, 0.6), + backgroundColor: alpha(theme.palette.primary.light, 0.6), '&:first-child': startBorderStyle, '&:last-child': endBorderStyle, }, @@ -66,7 +67,7 @@ const useStyles = makeStyles( }, }, dayInsideRangeInterval: { - color: theme.palette.getContrastText(fade(theme.palette.primary.light, 0.6)), + color: theme.palette.getContrastText(alpha(theme.palette.primary.light, 0.6)), }, notSelectedDate: { backgroundColor: 'transparent', @@ -80,7 +81,6 @@ const useStyles = makeStyles( border: `2px dashed ${theme.palette.divider}`, borderLeftColor: 'transparent', borderRightColor: 'transparent', - '&$rangeIntervalDayPreviewStart': { borderLeftColor: theme.palette.divider, ...startBorderStyle, @@ -92,15 +92,20 @@ const useStyles = makeStyles( }, rangeIntervalDayPreviewStart: {}, rangeIntervalDayPreviewEnd: {}, - }), - { name: 'MuiPickersDateRangeDay' } -); + }); -export function PureDateRangeDay(props: DateRangeDayProps) { +/** + * @ignore - do not document. + */ +const DateRangePickerDay = React.forwardRef(function DateRangePickerDay( + props: DateRangePickerDayProps & WithStyles, + ref: React.Ref, +) { const { + classes, className, day, - inCurrentMonth, + outsideCurrentMonth, isEndOfHighlighting, isEndOfPreviewing, isHighlighting, @@ -110,19 +115,18 @@ export function PureDateRangeDay(props: DateRangeDayProps) { selected, ...other } = props; - const utils = useUtils(); - const classes = useStyles(); + const utils = useUtils(); const isEndOfMonth = utils.isSameDay(day, utils.endOfMonth(day)); const isStartOfMonth = utils.isSameDay(day, utils.startOfMonth(day)); - const shouldRenderHighlight = isHighlighting && inCurrentMonth; - const shouldRenderPreview = isPreviewing && inCurrentMonth; + const shouldRenderHighlight = isHighlighting && !outsideCurrentMonth; + const shouldRenderPreview = isPreviewing && !outsideCurrentMonth; return (
(props: DateRangeDayProps) { [classes.rangeIntervalDayPreviewStart]: isStartOfPreviewing || isStartOfMonth, })} > - + {...other} + ref={ref} disableMargin allowSameDateSelection allowKeyboardControl={false} day={day} selected={selected} - inCurrentMonth={inCurrentMonth} + outsideCurrentMonth={outsideCurrentMonth} data-mui-test="DateRangeDay" - className={clsx( - classes.day, - { - [classes.notSelectedDate]: !selected, - [classes.dayOutsideRangeInterval]: !isHighlighting, - [classes.dayInsideRangeInterval]: !selected && isHighlighting, - }, - className - )} + className={clsx(classes.day, { + [classes.notSelectedDate]: !selected, + [classes.dayOutsideRangeInterval]: !isHighlighting, + [classes.dayInsideRangeInterval]: !selected && isHighlighting, + })} />
); -} +}); -PureDateRangeDay.displayName = 'DateRangeDay'; +(DateRangePickerDay as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The content of the component. + */ + children: PropTypes.node, + /** + * @ignore + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, + /** + * The date to show. + */ + day: PropTypes.any.isRequired, + /** + * @ignore + */ + isEndOfHighlighting: PropTypes.bool.isRequired, + /** + * @ignore + */ + isEndOfPreviewing: PropTypes.bool.isRequired, + /** + * @ignore + */ + isHighlighting: PropTypes.bool.isRequired, + /** + * @ignore + */ + isPreviewing: PropTypes.bool.isRequired, + /** + * @ignore + */ + isStartOfHighlighting: PropTypes.bool.isRequired, + /** + * @ignore + */ + isStartOfPreviewing: PropTypes.bool.isRequired, + /** + * If `true`, day is outside of month and will be hidden. + */ + outsideCurrentMonth: PropTypes.bool.isRequired, + /** + * If `true`, renders as selected. + */ + selected: PropTypes.bool, +}; -export const DateRangeDay = React.memo(PureDateRangeDay, (prevProps, nextProps) => { - return ( - prevProps.isHighlighting === nextProps.isHighlighting && - prevProps.isEndOfHighlighting === nextProps.isEndOfHighlighting && - prevProps.isStartOfHighlighting === nextProps.isStartOfHighlighting && - prevProps.isPreviewing === nextProps.isPreviewing && - prevProps.isEndOfPreviewing === nextProps.isEndOfPreviewing && - prevProps.isStartOfPreviewing === nextProps.isStartOfPreviewing && - areDayPropsEqual(prevProps, nextProps) - ); -}) as typeof PureDateRangeDay; +/** + * + * API: + * + * - [DateRangePickerDay API](https://material-ui.com/api/date-range-picker-day/) + */ +export default withStyles(styles, { name: 'MuiDateRangePickerDay' })( + React.memo(DateRangePickerDay, (prevProps, nextProps) => { + return ( + prevProps.isHighlighting === nextProps.isHighlighting && + prevProps.isEndOfHighlighting === nextProps.isEndOfHighlighting && + prevProps.isStartOfHighlighting === nextProps.isStartOfHighlighting && + prevProps.isPreviewing === nextProps.isPreviewing && + prevProps.isEndOfPreviewing === nextProps.isEndOfPreviewing && + prevProps.isStartOfPreviewing === nextProps.isStartOfPreviewing && + areDayPropsEqual(prevProps, nextProps) + ); + }), +) as ( + props: DateRangePickerDayProps & React.RefAttributes, +) => JSX.Element; diff --git a/packages/material-ui-lab/src/DateRangePickerDay/index.ts b/packages/material-ui-lab/src/DateRangePickerDay/index.ts new file mode 100644 index 00000000000000..52cf5ab8d18210 --- /dev/null +++ b/packages/material-ui-lab/src/DateRangePickerDay/index.ts @@ -0,0 +1,2 @@ +export * from './DateRangePickerDay'; +export { default } from './DateRangePickerDay'; diff --git a/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.spec.tsx b/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.spec.tsx new file mode 100644 index 00000000000000..3cd3d75643d575 --- /dev/null +++ b/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.spec.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import moment from 'moment'; +import { DateTimePicker } from '@material-ui/lab'; + + date?.set({ second: 0 })} + renderInput={() => } +/>; diff --git a/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.test.tsx b/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.test.tsx new file mode 100644 index 00000000000000..fae85c6221b90e --- /dev/null +++ b/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.test.tsx @@ -0,0 +1,262 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import { expect } from 'chai'; +import { spy, useFakeTimers, SinonSpy, SinonFakeTimers } from 'sinon'; +import { fireEvent, fireTouchChangedEvent, screen } from 'test/utils'; +import 'dayjs/locale/ru'; +import dayjs from 'dayjs'; +import MobileDateTimePicker from '@material-ui/lab/MobileDateTimePicker'; +import DesktopDateTimePicker from '@material-ui/lab/DesktopDateTimePicker'; +import StaticDateTimePicker from '@material-ui/lab/StaticDateTimePicker'; +import DayJsAdapter from '../dateAdapter/dayjs'; +import { adapterToUse, getByMuiTest, createPickerRender } from '../internal/pickers/test-utils'; + +describe('', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(new Date('2018-01-01T00:00:00.000').getTime()); + }); + + afterEach(() => { + clock.restore(); + }); + + const render = createPickerRender({ strict: false }); + + it('opens dialog on textField click for Mobile mode', () => { + render( + {}} + renderInput={(params) => } + />, + ); + + fireEvent.click(screen.getByRole('textbox')); + expect(screen.getByRole('dialog')).toBeVisible(); + }); + + it('opens dialog on calendar button click for Mobile mode', () => { + render( + {}} + renderInput={(params) => } + />, + ); + + fireEvent.click(screen.getByLabelText(/choose date/i)); + expect(screen.getByRole('dialog')).toBeVisible(); + }); + + it('allows to select full date end-to-end', function test() { + if (typeof window.Touch === 'undefined' || typeof window.TouchEvent === 'undefined') { + this.skip(); + } + + let onChangeMock: SinonSpy = spy(); + const clockTouchEvent = { + changedTouches: [ + { + clientX: 20, + clientY: 15, + }, + ], + }; + + function DateTimePickerWithState() { + const [date, setDate] = React.useState(null); + onChangeMock = spy(setDate); + + return ( + onChangeMock(newDate)} + renderInput={(params) => } + /> + ); + } + + render(); + fireEvent.click(screen.getByLabelText(/choose date/i)); + + expect(getByMuiTest('datetimepicker-toolbar-date')).to.have.text('Enter Date'); + expect(getByMuiTest('hours')).to.have.text('--'); + expect(getByMuiTest('minutes')).to.have.text('--'); + + // 1. Year view + fireEvent.click(screen.getByLabelText(/switch to year view/)); + fireEvent.click(screen.getByText('2010', { selector: 'button' })); + + expect(getByMuiTest('datetimepicker-toolbar-year')).to.have.text('2010'); + + // 2. Date + fireEvent.click(screen.getByLabelText('Jan 15, 2010')); + + expect(getByMuiTest('datetimepicker-toolbar-date')).to.have.text('Jan 15'); + + // 3. Hours + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockTouchEvent); + fireTouchChangedEvent(getByMuiTest('clock'), 'touchend', clockTouchEvent); + + expect(getByMuiTest('hours')).to.have.text('11'); + + // 4. Minutes + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockTouchEvent); + fireTouchChangedEvent(getByMuiTest('clock'), 'touchend', clockTouchEvent); + + expect(getByMuiTest('minutes')).to.have.text('53'); + + fireEvent.click(screen.getByText(/ok/i)); + expect(onChangeMock.calledWith(new Date('2010-01-15T11:53:00.000'))).to.be.equal(true); + }); + + it('prop: open – overrides open state', () => { + render( + } + open + onChange={() => {}} + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + />, + ); + + expect(screen.getByRole('dialog')).toBeVisible(); + }); + + it('prop: onCloseMock – dispatches on close request', () => { + const onCloseMock = spy(); + render( + } + open + onClose={onCloseMock} + onChange={() => {}} + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + />, + ); + + fireEvent.click(screen.getByText('Cancel')); + expect(onCloseMock.callCount).to.equal(1); + }); + + it('prop: dateAdapter – allows to override date adapter with prop', () => { + render( + } + onChange={() => {}} + dateAdapter={new DayJsAdapter({ locale: 'ru' })} + disableMaskedInput + value={dayjs('2018-01-15T00:00:00.000Z')} + />, + ); + + expect(screen.getByText('январь')).toBeVisible(); + }); + + it('prop: mask – should take the mask prop into account', () => { + render( + } + ampm={false} + inputFormat="mm.dd.yyyy hh:mm" + mask="__.__.____ __:__" + onChange={() => {}} + value={null} + />, + ); + + const textbox = screen.getByRole('textbox') as HTMLInputElement; + fireEvent.change(textbox, { + target: { + value: '12', + }, + }); + + expect(textbox.value).to.equal('12.'); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('prop: maxDateTime – minutes is disabled by date part', () => { + render( + {}} + renderInput={(params) => } + value={adapterToUse.date('2018-01-01T12:00:00.000Z')} + minDateTime={adapterToUse.date('2018-01-01T12:30:00.000Z')} + />, + ); + + expect(screen.getByLabelText('25 minutes')).to.have.attribute('aria-disabled', 'true'); + expect(screen.getByLabelText('35 minutes')).to.have.attribute('aria-disabled', 'false'); + }); + + it('prop: minDateTime – hours is disabled by date part', () => { + render( + {}} + ampm={false} + renderInput={(params) => } + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + minDateTime={adapterToUse.date('2018-01-01T12:30:00.000Z')} + />, + ); + + expect(screen.getByLabelText('11 hours')).to.have.attribute('aria-disabled', 'true'); + }); + + it('shows ArrowSwitcher on ClockView disabled and not allows to return back to the date', () => { + render( + {}} + renderInput={(params) => } + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + />, + ); + + expect(screen.getByLabelText('open previous view')).to.have.attribute('disabled'); + }); + + it('allows to switch using ArrowSwitcher on ClockView', () => { + render( + {}} + renderInput={(params) => } + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + />, + ); + + fireEvent.click(screen.getByLabelText('open next view')); + expect(screen.getByLabelText('open next view')).to.have.attribute('disabled'); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('allows to select the same day and move to the next view', () => { + const onChangeMock = spy(); + render( + } + value={adapterToUse.date('2018-01-01T00:00:00.000Z')} + />, + ); + + fireEvent.click(screen.getByLabelText('Jan 1, 2018')); + expect(onChangeMock.callCount).to.equal(1); + + expect(screen.getByLabelText(/Selected time/)).toBeVisible(); + }); +}); diff --git a/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.tsx b/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.tsx new file mode 100644 index 00000000000000..9fe1e9dda05cb3 --- /dev/null +++ b/packages/material-ui-lab/src/DateTimePicker/DateTimePicker.tsx @@ -0,0 +1,570 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import DateTimePickerToolbar from './DateTimePickerToolbar'; +import { ExportedClockPickerProps } from '../ClockPicker/ClockPicker'; +import { ResponsiveWrapper } from '../internal/pickers/wrappers/ResponsiveWrapper'; +import { pick12hOr24hFormat } from '../internal/pickers/text-field-helper'; +import { + useParsedDate, + OverrideParsableDateProps, +} from '../internal/pickers/hooks/date-helpers-hooks'; +import { ExportedDayPickerProps } from '../DayPicker/DayPicker'; +import { + makePickerWithStateAndWrapper, + SharedPickerProps, +} from '../internal/pickers/Picker/makePickerWithState'; +import { SomeWrapper } from '../internal/pickers/wrappers/Wrapper'; +import { WithViewsProps, AllSharedPickerProps } from '../internal/pickers/Picker/SharedPickerProps'; +import { DateAndTimeValidationError, validateDateAndTime } from './date-time-utils'; +import { makeValidationHook, ValidationProps } from '../internal/pickers/hooks/useValidation'; +import { + ParsableDate, + defaultMinDate, + defaultMaxDate, +} from '../internal/pickers/constants/prop-types'; + +type DateTimePickerViewsProps = OverrideParsableDateProps< + TDate, + ExportedClockPickerProps & ExportedDayPickerProps, + 'minDate' | 'maxDate' | 'minTime' | 'maxTime' +>; + +export interface BaseDateTimePickerProps + extends WithViewsProps<'year' | 'date' | 'month' | 'hours' | 'minutes'>, + ValidationProps, + DateTimePickerViewsProps { + /** + * To show tabs. + */ + hideTabs?: boolean; + /** + * Date tab icon. + */ + dateRangeIcon?: React.ReactNode; + /** + * Time tab icon. + */ + timeIcon?: React.ReactNode; + /** + * Minimal selectable moment of time with binding to date, to set min time in each day use `minTime`. + */ + minDateTime?: ParsableDate; + /** + * Minimal selectable moment of time with binding to date, to set max time in each day use `maxTime`. + */ + maxDateTime?: ParsableDate; + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat?: string; +} + +function useInterceptProps({ + ampm, + inputFormat, + maxDate: __maxDate = defaultMaxDate, + maxDateTime: __maxDateTime, + maxTime: __maxTime, + minDate: __minDate = defaultMinDate, + minDateTime: __minDateTime, + minTime: __minTime, + openTo = 'date', + orientation = 'portrait', + views = ['year', 'date', 'hours', 'minutes'], + ...other +}: BaseDateTimePickerProps & AllSharedPickerProps) { + const utils = useUtils(); + const minTime = useParsedDate(__minTime); + const maxTime = useParsedDate(__maxTime); + const minDate = useParsedDate(__minDate); + const maxDate = useParsedDate(__maxDate); + const minDateTime = useParsedDate(__minDateTime); + const maxDateTime = useParsedDate(__maxDateTime); + const willUseAmPm = ampm ?? utils.is12HourCycleInCurrentLocale(); + + if (orientation !== 'portrait') { + throw new Error('We are not supporting custom orientation for DateTimePicker yet :('); + } + + return { + openTo, + views, + ampm: willUseAmPm, + ampmInClock: true, + orientation, + showToolbar: true, + showTabs: true, + allowSameDateSelection: true, + minDate: minDateTime || minDate, + minTime: minDateTime || minTime, + maxDate: maxDateTime || maxDate, + maxTime: maxDateTime || maxTime, + disableIgnoringDatePartForTimeValidation: Boolean(minDateTime || maxDateTime), + acceptRegex: willUseAmPm ? /[\dap]/gi : /\d/gi, + mask: '__/__/____ __:__', + disableMaskedInput: willUseAmPm, + inputFormat: pick12hOr24hFormat(inputFormat, willUseAmPm, { + localized: utils.formats.keyboardDateTime, + '12h': utils.formats.keyboardDateTime12h, + '24h': utils.formats.keyboardDateTime24h, + }), + ...other, + }; +} + +const useValidation = makeValidationHook< + DateAndTimeValidationError, + ParsableDate, + BaseDateTimePickerProps +>(validateDateAndTime); + +export const dateTimePickerConfig = { + useInterceptProps, + useValidation, + DefaultToolbarComponent: DateTimePickerToolbar, +}; + +export type DateTimePickerGenericComponent = ( + props: BaseDateTimePickerProps & SharedPickerProps, +) => JSX.Element; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const DateTimePicker = makePickerWithStateAndWrapper>( + ResponsiveWrapper, + { + name: 'MuiDateTimePicker', + ...dateTimePickerConfig, + }, +) as DateTimePickerGenericComponent; + +(DateTimePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * If `true`, `onChange` is fired on click even if the same date is selected. + * @default false + */ + allowSameDateSelection: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ + ampm: PropTypes.bool, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * "CANCEL" Text message + * @default "CANCEL" + */ + cancelText: PropTypes.node, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * If `true`, it shows the clear action in the picker dialog. + * @default false + */ + clearable: PropTypes.bool, + /** + * "CLEAR" Text message + * @default "CLEAR" + */ + clearText: PropTypes.node, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Date tab icon. + */ + dateRangeIcon: PropTypes.node, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * CSS media query when `Mobile` mode will be changed to `Desktop`. + * @default "@media (pointer: fine)" + * @example "@media (min-width: 720px)" or theme.breakpoints.up("sm") + */ + desktopModeMediaQuery: PropTypes.string, + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * If `true`, todays date is rendering without highlighting with circle. + * @default false + */ + disableHighlightToday: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * Get aria-label text for switching between views button. + */ + getViewSwitchingButtonText: PropTypes.func, + /** + * To show tabs. + */ + hideTabs: PropTypes.bool, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Minimal selectable moment of time with binding to date, to set max time in each day use `maxTime`. + */ + maxDateTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + maxTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Minimal selectable moment of time with binding to date, to set min time in each day use `minTime`. + */ + minDateTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * "OK" button text. + * @default "OK" + */ + okText: PropTypes.node, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Callback firing on year change @DateIOType. + */ + onYearChange: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * First view to show. + */ + openTo: PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'seconds', 'year']), + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Popper props passed down to [Popper](https://material-ui.com/api/popper/) component. + */ + PopperProps: PropTypes.object, + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Custom renderer for day. Check [DayComponentProps api](https://material-ui-pickers.dev/api/Day) @DateIOType. + */ + renderDay: PropTypes.func, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. + * @default false + */ + showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. + * @default false + */ + showTodayButton: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Time tab icon. + */ + timeIcon: PropTypes.node, + /** + * "TODAY" Text message + * @default "TODAY" + */ + todayText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * Custom component for popper [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). + */ + TransitionComponent: PropTypes.elementType, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Array of views to show. + */ + views: PropTypes.arrayOf( + PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'year']).isRequired, + ), +}; + +export type DateTimePickerProps = React.ComponentProps; + +export default DateTimePicker; diff --git a/packages/pickers/lib/src/DateTimePicker/DateTimePickerTabs.tsx b/packages/material-ui-lab/src/DateTimePicker/DateTimePickerTabs.tsx similarity index 55% rename from packages/pickers/lib/src/DateTimePicker/DateTimePickerTabs.tsx rename to packages/material-ui-lab/src/DateTimePicker/DateTimePickerTabs.tsx index 4899ba39459bc1..6e72f22dfc87a2 100644 --- a/packages/pickers/lib/src/DateTimePicker/DateTimePickerTabs.tsx +++ b/packages/material-ui-lab/src/DateTimePicker/DateTimePickerTabs.tsx @@ -3,11 +3,11 @@ import clsx from 'clsx'; import Tab from '@material-ui/core/Tab'; import Tabs from '@material-ui/core/Tabs'; import Paper from '@material-ui/core/Paper'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import { TimeIcon } from '../_shared/icons/Time'; -import { DateTimePickerView } from './DateTimePicker'; -import { DateRangeIcon } from '../_shared/icons/DateRange'; -import { WrapperVariantContext } from '../wrappers/WrapperVariantContext'; +import { createStyles, WithStyles, withStyles, Theme, useTheme } from '@material-ui/core/styles'; +import TimeIcon from '../internal/svg-icons/Time'; +import DateRangeIcon from '../internal/svg-icons/DateRange'; +import { WrapperVariantContext } from '../internal/pickers/wrappers/WrapperVariantContext'; +import { DateTimePickerView } from '../internal/pickers/typings/Views'; const viewToTabIndex = (openView: DateTimePickerView) => { if (openView === 'date' || openView === 'year') { @@ -32,33 +32,41 @@ export interface DateTimePickerTabsProps { view: DateTimePickerView; } -export const useStyles = makeStyles( - (theme) => { - const tabsBackground = - theme.palette.type === 'light' - ? theme.palette.primary.main - : theme.palette.background.default; +export const styles = (theme: Theme) => { + const tabsBackground = + theme.palette.mode === 'light' ? theme.palette.primary.main : theme.palette.background.default; - return { - root: {}, - modeDesktop: { - order: 1, - }, - tabs: { - color: theme.palette.getContrastText(tabsBackground), - backgroundColor: tabsBackground, - }, - }; - }, - { name: 'MuiDateTimePickerTabs' } -); + return createStyles({ + root: {}, + modeDesktop: { + order: 1, + }, + tabs: { + color: theme.palette.getContrastText(tabsBackground), + backgroundColor: tabsBackground, + }, + }); +}; + +export type DateTimePickerTabsClassKey = keyof WithStyles['classes']; + +/** + * @ignore - internal component. + */ +const DateTimePickerTabs: React.FC> = ( + props, +) => { + const { + classes, + dateRangeIcon = , + onChange, + timeIcon = , + view, + } = props; -const DateTimePickerTabs: React.FC = (props) => { - const { dateRangeIcon = , onChange, timeIcon = , view } = props; - const classes = useStyles(); const theme = useTheme(); const wrapperVariant = React.useContext(WrapperVariantContext); - const indicatorColor = theme.palette.type === 'light' ? 'secondary' : 'primary'; + const indicatorColor = theme.palette.mode === 'light' ? 'secondary' : 'primary'; const handleChange = (e: React.ChangeEvent<{}>, value: DateTimePickerView) => { if (value !== viewToTabIndex(view)) { @@ -90,4 +98,4 @@ const DateTimePickerTabs: React.FC = (props) => { ); }; -export default DateTimePickerTabs; +export default withStyles(styles, { name: 'MuiDateTimePickerTabs' })(DateTimePickerTabs); diff --git a/packages/material-ui-lab/src/DateTimePicker/DateTimePickerToolbar.tsx b/packages/material-ui-lab/src/DateTimePicker/DateTimePickerToolbar.tsx new file mode 100644 index 00000000000000..94da6a71b6ae01 --- /dev/null +++ b/packages/material-ui-lab/src/DateTimePicker/DateTimePickerToolbar.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; +import ToolbarText from '../internal/pickers/PickersToolbarText'; +import PickerToolbar from '../internal/pickers/PickersToolbar'; +import ToolbarButton from '../internal/pickers/PickersToolbarButton'; +import DateTimePickerTabs from './DateTimePickerTabs'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import { WrapperVariantContext } from '../internal/pickers/wrappers/WrapperVariantContext'; +import { ToolbarComponentProps } from '../internal/pickers/typings/BasePicker'; +import { DateTimePickerView } from '../internal/pickers/typings/Views'; + +export const styles = createStyles({ + root: { + paddingLeft: 16, + paddingRight: 16, + justifyContent: 'space-around', + }, + separator: { + margin: '0 4px 0 2px', + cursor: 'default', + }, + timeContainer: { + display: 'flex', + }, + dateContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + }, + timeTypography: {}, + penIcon: { + position: 'absolute', + top: 8, + right: 8, + }, +}); + +export type DateTimePickerToolbarClassKey = keyof WithStyles['classes']; + +/** + * @ignore - internal component. + */ +const DateTimePickerToolbar: React.FC> = ( + props, +) => { + const { + ampm, + date, + dateRangeIcon, + classes, + hideTabs, + isMobileKeyboardViewOpen, + onChange, + openView, + setOpenView, + timeIcon, + toggleMobileKeyboardView, + toolbarFormat, + toolbarPlaceholder = '––', + toolbarTitle = 'SELECT DATE & TIME', + ...other + } = props; + const utils = useUtils(); + const wrapperVariant = React.useContext(WrapperVariantContext); + const showTabs = + wrapperVariant === 'desktop' + ? true + : !hideTabs && typeof window !== 'undefined' && window.innerHeight > 667; + + const formatHours = (time: unknown) => + ampm ? utils.format(time, 'hours12h') : utils.format(time, 'hours24h'); + + const dateText = React.useMemo(() => { + if (!date) { + return toolbarPlaceholder; + } + + if (toolbarFormat) { + return utils.formatByString(date, toolbarFormat); + } + + return utils.format(date, 'shortDate'); + }, [date, toolbarFormat, toolbarPlaceholder, utils]); + + return ( + + {wrapperVariant !== 'desktop' && ( + +
+ setOpenView('year')} + selected={openView === 'year'} + value={date ? utils.format(date, 'year') : '–'} + /> + setOpenView('date')} + selected={openView === 'date'} + value={dateText} + /> +
+
+ setOpenView('hours')} + selected={openView === 'hours'} + value={date ? formatHours(date) : '--'} + typographyClassName={classes.timeTypography} + /> + + setOpenView('minutes')} + selected={openView === 'minutes'} + value={date ? utils.format(date, 'minutes') : '--'} + typographyClassName={classes.timeTypography} + /> +
+
+ )} + {showTabs && ( + + )} +
+ ); +}; + +export default withStyles(styles, { name: 'MuiDateTimePickerToolbar' })(DateTimePickerToolbar); diff --git a/packages/pickers/lib/src/DateTimePicker/date-time-utils.ts b/packages/material-ui-lab/src/DateTimePicker/date-time-utils.ts similarity index 54% rename from packages/pickers/lib/src/DateTimePicker/date-time-utils.ts rename to packages/material-ui-lab/src/DateTimePicker/date-time-utils.ts index 1a6f851f7b3655..cf44776c6cc370 100644 --- a/packages/pickers/lib/src/DateTimePicker/date-time-utils.ts +++ b/packages/material-ui-lab/src/DateTimePicker/date-time-utils.ts @@ -1,11 +1,11 @@ -import { ParsableDate } from '../constants/prop-types'; -import { MuiPickersAdapter } from '../_shared/hooks/useUtils'; -import { DateValidationProps, validateDate } from '../_helpers/date-utils'; -import { TimeValidationProps, validateTime } from '../_helpers/time-utils'; +import { ParsableDate } from '../internal/pickers/constants/prop-types'; +import { MuiPickersAdapter } from '../internal/pickers/hooks/useUtils'; +import { DateValidationProps, validateDate } from '../internal/pickers/date-utils'; +import { TimeValidationProps, validateTime } from '../internal/pickers/time-utils'; export function validateDateAndTime( - utils: MuiPickersAdapter, - value: unknown | ParsableDate, + utils: MuiPickersAdapter, + value: ParsableDate, { minDate, maxDate, @@ -13,7 +13,7 @@ export function validateDateAndTime( shouldDisableDate, disablePast, ...timeValidationProps - }: DateValidationProps & TimeValidationProps + }: DateValidationProps & TimeValidationProps, ) { const dateValidationResult = validateDate(utils, value, { minDate, diff --git a/packages/material-ui-lab/src/DateTimePicker/index.ts b/packages/material-ui-lab/src/DateTimePicker/index.ts new file mode 100644 index 00000000000000..2d763f4b586cdb --- /dev/null +++ b/packages/material-ui-lab/src/DateTimePicker/index.ts @@ -0,0 +1,2 @@ +export * from './DateTimePicker'; +export { default } from './DateTimePicker'; diff --git a/packages/material-ui-lab/src/DayPicker/DayPicker.test.tsx b/packages/material-ui-lab/src/DayPicker/DayPicker.test.tsx new file mode 100644 index 00000000000000..451f5234602f24 --- /dev/null +++ b/packages/material-ui-lab/src/DayPicker/DayPicker.test.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { getClasses, createMount, fireEvent, screen, describeConformance } from 'test/utils'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import DayPicker from '@material-ui/lab/DayPicker'; +import { createPickerRender, getAllByMuiTest } from '../internal/pickers/test-utils'; + +describe('', () => { + const mount = createMount(); + const render = createPickerRender({ strict: false }); + let classes: Record; + + const localizedMount = (node: React.ReactNode) => { + return mount({node}); + }; + + before(() => { + classes = getClasses( {}} />); + }); + + describeConformance( {}} />, () => ({ + classes, + inheritComponent: 'div', + mount: localizedMount, + refInstanceof: window.HTMLDivElement, + // cannot test reactTestRenderer because of required context + skip: ['componentProp', 'propsSpread', 'reactTestRenderer'], + })); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('renders calendar standalone', () => { + render( {}} />); + + expect(screen.getByText('January')).toBeVisible(); + expect(screen.getByText('2019')).toBeVisible(); + expect(getAllByMuiTest('day')).to.have.length(31); + }); + + // TODO + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('renders year selection standalone', () => { + render( + {}} />, + ); + + expect(getAllByMuiTest('year')).to.have.length(200); + }); + + it('switches between views uncontrolled', () => { + render( {}} />); + + fireEvent.click(screen.getByLabelText(/switch to year view/i)); + + expect(screen.queryByLabelText(/switch to year view/i)).to.equal(null); + expect(screen.getByLabelText('year view is open, switch to calendar view')).toBeVisible(); + }); +}); diff --git a/packages/material-ui-lab/src/DayPicker/DayPicker.tsx b/packages/material-ui-lab/src/DayPicker/DayPicker.tsx new file mode 100644 index 00000000000000..f4e98bb5bf5971 --- /dev/null +++ b/packages/material-ui-lab/src/DayPicker/DayPicker.tsx @@ -0,0 +1,347 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import MonthPicker from '../MonthPicker/MonthPicker'; +import { useCalendarState } from './useCalendarState'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import FadeTransitionGroup from './PickersFadeTransitionGroup'; +import Calendar, { ExportedCalendarProps } from './PickersCalendar'; +import { PickerOnChangeFn, useViews } from '../internal/pickers/hooks/useViews'; +import { DAY_SIZE, DAY_MARGIN } from '../internal/pickers/constants/dimensions'; +import CalendarHeader, { ExportedCalendarHeaderProps } from './PickersCalendarHeader'; +import YearPicker, { ExportedYearPickerProps } from '../YearPicker/YearPicker'; +import { defaultMinDate, defaultMaxDate } from '../internal/pickers/constants/prop-types'; +import { IsStaticVariantContext } from '../internal/pickers/wrappers/WrapperVariantContext'; +import { DateValidationProps, findClosestEnabledDate } from '../internal/pickers/date-utils'; +import { DatePickerView } from '../internal/pickers/typings/Views'; +import PickerView from '../internal/pickers/Picker/PickerView'; + +export interface DayPickerProps + extends DateValidationProps, + ExportedCalendarProps, + ExportedYearPickerProps, + ExportedCalendarHeaderProps { + date: TDate | null; + /** Views for day picker. */ + views?: TView[]; + /** Controlled open view. */ + view?: TView; + /** Initially open view. */ + openTo?: TView; + /** Callback fired on view change. */ + onViewChange?: (view: TView) => void; + /** Callback fired on date change */ + onChange: PickerOnChangeFn; + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations?: boolean; + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange?: (date: TDate) => void; + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth?: TDate; + className?: string; +} + +export type ExportedDayPickerProps = Omit< + DayPickerProps, + | 'date' + | 'view' + | 'views' + | 'openTo' + | 'onChange' + | 'changeView' + | 'slideDirection' + | 'currentMonth' + | 'className' +>; + +export const styles = createStyles({ + root: { + display: 'flex', + flexDirection: 'column', + }, + viewTransitionContainer: { + overflowY: 'auto', + }, + fullHeightContainer: { + flex: 1, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: (DAY_SIZE + DAY_MARGIN * 4) * 7, + height: '100%', + }, +}); + +export type DayPickerClassKey = keyof WithStyles['classes']; + +export const defaultReduceAnimations = + typeof navigator !== 'undefined' && /(android)/i.test(navigator.userAgent); + +/** + * @ignore - do not document. + */ +const DayPicker = React.forwardRef(function DayPicker< + TDate extends any, + TView extends DatePickerView = DatePickerView +>(props: DayPickerProps & WithStyles, ref: React.Ref) { + const { + allowKeyboardControl: allowKeyboardControlProp, + onViewChange, + date, + disableFuture, + disablePast, + defaultCalendarMonth, + classes, + loading, + maxDate: maxDateProp, + minDate: minDateProp, + onChange, + onMonthChange, + reduceAnimations = defaultReduceAnimations, + renderLoading, + shouldDisableDate, + shouldDisableYear, + view, + views = ['year', 'date'] as TView[], + openTo = 'date' as TView, + className, + ...other + } = props; + + const utils = useUtils(); + const isStatic = React.useContext(IsStaticVariantContext); + const allowKeyboardControl = allowKeyboardControlProp ?? !isStatic; + + const minDate = minDateProp || utils.date(defaultMinDate)!; + const maxDate = maxDateProp || utils.date(defaultMaxDate)!; + + const { openView, setOpenView } = useViews({ + view, + views, + openTo, + onChange, + onViewChange, + }); + + const { + calendarState, + changeFocusedDay, + changeMonth, + isDateDisabled, + handleChangeMonth, + onMonthSwitchingAnimationEnd, + } = useCalendarState({ + date, + defaultCalendarMonth, + reduceAnimations, + onMonthChange, + minDate, + maxDate, + shouldDisableDate, + disablePast, + disableFuture, + }); + + React.useEffect(() => { + if (date && isDateDisabled(date)) { + const closestEnabledDate = findClosestEnabledDate({ + utils, + date, + minDate, + maxDate, + disablePast: Boolean(disablePast), + disableFuture: Boolean(disableFuture), + shouldDisableDate: isDateDisabled, + }); + + onChange(closestEnabledDate, 'partial'); + } + // This call is too expensive to run it on each prop change. + // So just ensure that we are not rendering disabled as selected on mount. + }, []); // eslint-disable-line + + React.useEffect(() => { + if (date) { + changeMonth(date); + } + }, [date]); // eslint-disable-line + + return ( + + void} + onMonthChange={(newMonth, direction) => handleChangeMonth({ newMonth, direction })} + minDate={minDate} + maxDate={maxDate} + disablePast={disablePast} + disableFuture={disableFuture} + reduceAnimations={reduceAnimations} + /> + +
+ {openView === 'year' && ( + + )} + + {openView === 'month' && ( + + )} + + {openView === 'date' && ( + + )} +
+
+
+ ); +}); + +(DayPicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * @ignore + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, + /** + * @ignore + */ + date: PropTypes.any, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Max selectable date. @DateIOType + * @default Date(2099-31-12) + */ + maxDate: PropTypes.any, + /** + * Min selectable date. @DateIOType + * @default Date(1900-01-01) + */ + minDate: PropTypes.any, + /** + * Callback fired on date change + */ + onChange: PropTypes.func.isRequired, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Initially open view. + */ + openTo: PropTypes.oneOf(['date', 'month', 'year']), + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * Controlled open view. + */ + view: PropTypes.oneOf(['date', 'month', 'year']), + /** + * Views for day picker. + */ + views: PropTypes.arrayOf(PropTypes.oneOf(['date', 'month', 'year']).isRequired), +}; + +export default withStyles(styles, { name: 'MuiDayPicker' })(DayPicker) as ( + props: DayPickerProps & React.RefAttributes, +) => JSX.Element; diff --git a/packages/pickers/lib/src/views/Calendar/Calendar.tsx b/packages/material-ui-lab/src/DayPicker/PickersCalendar.tsx similarity index 77% rename from packages/pickers/lib/src/views/Calendar/Calendar.tsx rename to packages/material-ui-lab/src/DayPicker/PickersCalendar.tsx index e228df7478e571..fbf12e621914f2 100644 --- a/packages/pickers/lib/src/views/Calendar/Calendar.tsx +++ b/packages/material-ui-lab/src/DayPicker/PickersCalendar.tsx @@ -1,19 +1,18 @@ import * as React from 'react'; import clsx from 'clsx'; import Typography from '@material-ui/core/Typography'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import { Day, DayProps } from './Day'; -import { useUtils, useNow } from '../../_shared/hooks/useUtils'; -import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; -import { useDefaultProps } from '../../_shared/withDefaultProps'; -import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; -import { useGlobalKeyDown, keycode } from '../../_shared/hooks/useKeyDown'; -import { SlideTransition, SlideDirection, SlideTransitionProps } from './SlideTransition'; +import { createStyles, WithStyles, withStyles, Theme, useTheme } from '@material-ui/core/styles'; +import PickersDay, { PickersDayProps } from '../PickersDay/PickersDay'; +import { useUtils, useNow } from '../internal/pickers/hooks/useUtils'; +import { PickerOnChangeFn } from '../internal/pickers/hooks/useViews'; +import { DAY_SIZE, DAY_MARGIN } from '../internal/pickers/constants/dimensions'; +import { PickerSelectionState } from '../internal/pickers/hooks/usePickerState'; +import { useGlobalKeyDown, keycode } from '../internal/pickers/hooks/useKeyDown'; +import SlideTransition, { SlideDirection, SlideTransitionProps } from './PickersSlideTransition'; export interface ExportedCalendarProps extends Pick< - DayProps, + PickersDayProps, 'disableHighlightToday' | 'showDaysOutsideCurrentMonth' | 'allowSameDateSelection' > { /** @@ -25,48 +24,44 @@ export interface ExportedCalendarProps */ renderDay?: ( day: TDate, - selectedDates: (TDate | null)[], - DayComponentProps: DayProps + selectedDates: Array, + DayComponentProps: PickersDayProps, ) => JSX.Element; /** * Enables keyboard listener for moving between days in calendar. - * * @default currentWrapper !== 'static' */ allowKeyboardControl?: boolean; /** * If `true` renders `LoadingComponent` in calendar instead of calendar view. * Can be used to preload information and show it in calendar. - * * @default false */ loading?: boolean; /** * Component displaying when passed `loading` true. - * * @default () => "..." */ renderLoading?: () => React.ReactNode; } -export interface CalendarProps extends ExportedCalendarProps { +export interface PickersCalendarProps extends ExportedCalendarProps { date: TDate | null | Array; isDateDisabled: (day: TDate) => boolean; slideDirection: SlideDirection; currentMonth: TDate; reduceAnimations: boolean; focusedDay: TDate | null; - changeFocusedDay: (newFocusedDay: TDate) => void; + onFocusedDayChange: (newFocusedDay: TDate) => void; isMonthSwitchingAnimating: boolean; onMonthSwitchingAnimationEnd: () => void; TransitionProps?: Partial; className?: string; } -const muiComponentConfig = { name: 'MuiPickersCalendar' }; -export const useStyles = makeStyles((theme) => { - const weeksContainerHeight = (DAY_SIZE + DAY_MARGIN * 4) * 6; - return { +const weeksContainerHeight = (DAY_SIZE + DAY_MARGIN * 4) * 6; +export const styles = (theme: Theme) => + createStyles({ root: { minHeight: weeksContainerHeight, }, @@ -106,14 +101,19 @@ export const useStyles = makeStyles((theme) => { alignItems: 'center', color: theme.palette.text.secondary, }, - }; -}, muiComponentConfig); + }); + +export type PickersCalendarClassKey = keyof WithStyles['classes']; -export function Calendar(props: CalendarProps) { +/** + * @ignore - do not document. + */ +function PickersCalendar(props: PickersCalendarProps & WithStyles) { const { allowKeyboardControl, allowSameDateSelection, - changeFocusedDay, + onFocusedDayChange: changeFocusedDay, + classes, className, currentMonth, date, @@ -130,12 +130,11 @@ export function Calendar(props: CalendarProps) { showDaysOutsideCurrentMonth, slideDirection, TransitionProps, - } = useDefaultProps(props, muiComponentConfig); + } = props; const now = useNow(); const utils = useUtils(); const theme = useTheme(); - const classes = useStyles(); const handleDaySelect = React.useCallback( (day: TDate, isFinish: PickerSelectionState = 'finish') => { @@ -144,7 +143,7 @@ export function Calendar(props: CalendarProps) { onChange(finalDate, isFinish); }, - [date, now, onChange, utils] + [date, now, onChange, utils], ); const initialDate = Array.isArray(date) ? date[0] : date; @@ -182,6 +181,7 @@ export function Calendar(props: CalendarProps) { ))}
+ {loading ? (
{renderLoading()}
) : ( @@ -193,19 +193,16 @@ export function Calendar(props: CalendarProps) { className={clsx(classes.root, className)} {...TransitionProps} > -
+
{utils.getWeekArray(currentMonth).map((week) => (
{week.map((day) => { - const disabled = isDateDisabled(day); - const isDayInCurrentMonth = utils.getMonth(day) === currentMonthNumber; - - const dayProps: DayProps = { + const dayProps: PickersDayProps = { key: (day as any)?.toString(), day, role: 'cell', isAnimating: isMonthSwitchingAnimating, - disabled, + disabled: isDateDisabled(day), allowKeyboardControl, allowSameDateSelection, focused: @@ -213,9 +210,9 @@ export function Calendar(props: CalendarProps) { Boolean(focusedDay) && utils.isSameDay(day, nowFocusedDay), today: utils.isSameDay(day, now), - inCurrentMonth: isDayInCurrentMonth, + outsideCurrentMonth: utils.getMonth(day) !== currentMonthNumber, selected: selectedDates.some( - (selectedDate) => selectedDate && utils.isSameDay(selectedDate, day) + (selectedDate) => selectedDate && utils.isSameDay(selectedDate, day), ), disableHighlightToday, showDaysOutsideCurrentMonth, @@ -230,7 +227,7 @@ export function Calendar(props: CalendarProps) { return renderDay ? ( renderDay(day, selectedDates, dayProps) ) : ( - + ); })}
@@ -242,4 +239,6 @@ export function Calendar(props: CalendarProps) { ); } -Calendar.displayName = 'Calendar'; +export default withStyles(styles, { name: 'MuiPickersCalendar' })(PickersCalendar) as ( + props: PickersCalendarProps, +) => JSX.Element; diff --git a/packages/pickers/lib/src/views/Calendar/CalendarHeader.tsx b/packages/material-ui-lab/src/DayPicker/PickersCalendarHeader.tsx similarity index 75% rename from packages/pickers/lib/src/views/Calendar/CalendarHeader.tsx rename to packages/material-ui-lab/src/DayPicker/PickersCalendarHeader.tsx index d6cb89d5ebde62..3bd1bf3dede347 100644 --- a/packages/pickers/lib/src/views/Calendar/CalendarHeader.tsx +++ b/packages/material-ui-lab/src/DayPicker/PickersCalendarHeader.tsx @@ -1,24 +1,27 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import clsx from 'clsx'; import Fade from '@material-ui/core/Fade'; -import { makeStyles } from '@material-ui/core/styles'; +import { createStyles, WithStyles, withStyles, Theme } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; -import { DatePickerView } from '../../DatePicker'; -import { SlideDirection } from './SlideTransition'; -import { useUtils } from '../../_shared/hooks/useUtils'; -import { FadeTransitionGroup } from './FadeTransitionGroup'; -import { DateValidationProps } from '../../_helpers/date-utils'; -import { ArrowDropDownIcon } from '../../_shared/icons/ArrowDropDown'; -import { ArrowSwitcher, ExportedArrowSwitcherProps } from '../../_shared/ArrowSwitcher'; +import { SlideDirection } from './PickersSlideTransition'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import FadeTransitionGroup from './PickersFadeTransitionGroup'; +import { DateValidationProps } from '../internal/pickers/date-utils'; +// tslint:disable-next-line no-relative-import-in-test +import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown'; +import ArrowSwitcher, { + ExportedArrowSwitcherProps, +} from '../internal/pickers/PickersArrowSwitcher'; import { usePreviousMonthDisabled, useNextMonthDisabled, -} from '../../_shared/hooks/date-helpers-hooks'; +} from '../internal/pickers/hooks/date-helpers-hooks'; +import { DatePickerView } from '../internal/pickers/typings/Views'; export type ExportedCalendarHeaderProps = Pick< - CalendarHeaderProps, + PickersCalendarHeaderProps, | 'leftArrowIcon' | 'rightArrowIcon' | 'leftArrowButtonProps' @@ -28,10 +31,10 @@ export type ExportedCalendarHeaderProps = Pick< | 'getViewSwitchingButtonText' >; -export interface CalendarHeaderProps +export interface PickersCalendarHeaderProps extends ExportedArrowSwitcherProps, Omit, 'shouldDisableDate'> { - view: DatePickerView; + openView: DatePickerView; views: DatePickerView[]; currentMonth: TDate; /** @@ -39,12 +42,12 @@ export interface CalendarHeaderProps */ getViewSwitchingButtonText?: (currentView: DatePickerView) => string; reduceAnimations: boolean; - changeView: (view: DatePickerView) => void; + onViewChange?: (view: DatePickerView) => void; onMonthChange: (date: TDate, slideDirection: SlideDirection) => void; } -export const useStyles = makeStyles( - (theme) => ({ +export const styles = (theme: Theme) => + createStyles({ root: { display: 'flex', alignItems: 'center', @@ -80,9 +83,9 @@ export const useStyles = makeStyles( monthText: { marginRight: 4, }, - }), - { name: 'MuiPickersCalendarHeader' } -); + }); + +export type PickersCalendarHeaderClassKey = keyof WithStyles['classes']; function getSwitchingViewAriaText(view: DatePickerView) { return view === 'year' @@ -90,29 +93,34 @@ function getSwitchingViewAriaText(view: DatePickerView) { : 'calendar view is open, switch to year view'; } -export function CalendarHeader(props: CalendarHeaderProps) { +/** + * @ignore - do not document. + */ +function PickersCalendarHeader( + props: PickersCalendarHeaderProps & WithStyles, +) { const { - view: currentView, - views, + onViewChange, + classes, currentMonth: month, - changeView, - minDate, - maxDate, - disablePast, disableFuture, + disablePast, + getViewSwitchingButtonText = getSwitchingViewAriaText, + leftArrowButtonProps, + leftArrowButtonText = 'previous month', + leftArrowIcon, + maxDate, + minDate, onMonthChange, reduceAnimations, - leftArrowButtonProps, rightArrowButtonProps, - leftArrowIcon, - rightArrowIcon, - leftArrowButtonText = 'previous month', rightArrowButtonText = 'next month', - getViewSwitchingButtonText = getSwitchingViewAriaText, + rightArrowIcon, + openView: currentView, + views, } = props; const utils = useUtils(); - const classes = useStyles(); const selectNextMonth = () => onMonthChange(utils.getNextMonth(month), 'left'); const selectPreviousMonth = () => onMonthChange(utils.getPreviousMonth(month), 'right'); @@ -121,23 +129,23 @@ export function CalendarHeader(props: CalendarHeaderProps) { const isPreviousMonthDisabled = usePreviousMonthDisabled(month, { disablePast, minDate }); const toggleView = () => { - if (views.length === 1) { + if (views.length === 1 || !onViewChange) { return; } if (views.length === 2) { - changeView(views.find((view) => view !== currentView) || views[0]); + onViewChange(views.find((view) => view !== currentView) || views[0]); } else { // switching only between first 2 const nextIndexToOpen = views.indexOf(currentView) !== 0 ? 0 : 1; - changeView(views[nextIndexToOpen]); + onViewChange(views[nextIndexToOpen]); } }; return (
-
+
(props: CalendarHeaderProps) { ); } -CalendarHeader.displayName = 'PickersCalendarHeader'; - -CalendarHeader.propTypes = { +PickersCalendarHeader.propTypes = { leftArrowButtonText: PropTypes.string, leftArrowIcon: PropTypes.node, rightArrowButtonText: PropTypes.string, rightArrowIcon: PropTypes.node, }; -export default CalendarHeader; +export default withStyles(styles, { name: 'MuiPickersCalendarHeader' })(PickersCalendarHeader) as < + TDate +>( + props: PickersCalendarHeaderProps, +) => JSX.Element; diff --git a/packages/pickers/lib/src/views/Calendar/FadeTransitionGroup.tsx b/packages/material-ui-lab/src/DayPicker/PickersFadeTransitionGroup.tsx similarity index 53% rename from packages/pickers/lib/src/views/Calendar/FadeTransitionGroup.tsx rename to packages/material-ui-lab/src/DayPicker/PickersFadeTransitionGroup.tsx index cfc924f430a16e..772e429ed416aa 100644 --- a/packages/pickers/lib/src/views/Calendar/FadeTransitionGroup.tsx +++ b/packages/material-ui-lab/src/DayPicker/PickersFadeTransitionGroup.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; +import { createStyles, WithStyles, withStyles, Theme } from '@material-ui/core/styles'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; interface FadeTransitionProps { @@ -11,44 +11,45 @@ interface FadeTransitionProps { } const animationDuration = 500; -export const useStyles = makeStyles( - (theme) => { - return { - root: { - display: 'block', - position: 'relative', - }, - fadeEnter: { - willChange: 'transform', - opacity: 0, - }, - fadeEnterActive: { - opacity: 1, - transition: theme.transitions.create('opacity', { - duration: animationDuration, - }), - }, - fadeExit: { - opacity: 1, - }, - fadeExitActive: { - opacity: 0, - transition: theme.transitions.create('opacity', { - duration: animationDuration / 2, - }), - }, - }; - }, - { name: 'MuiPickersFadeTransition' } -); +export const styles = (theme: Theme) => + createStyles({ + root: { + display: 'block', + position: 'relative', + }, + fadeEnter: { + willChange: 'transform', + opacity: 0, + }, + fadeEnterActive: { + opacity: 1, + transition: theme.transitions.create('opacity', { + duration: animationDuration, + }), + }, + fadeExit: { + opacity: 1, + }, + fadeExitActive: { + opacity: 0, + transition: theme.transitions.create('opacity', { + duration: animationDuration / 2, + }), + }, + }); -export const FadeTransitionGroup: React.FC = ({ +export type PickersFadeTransitionGroupClassKey = keyof WithStyles['classes']; + +/** + * @ignore - do not document. + */ +const FadeTransitionGroup: React.FC> = ({ + classes, children, className, reduceAnimations, transKey, }) => { - const classes = useStyles(); if (reduceAnimations) { return children; } @@ -81,3 +82,5 @@ export const FadeTransitionGroup: React.FC = ({ ); }; + +export default withStyles(styles, { name: 'MuiPickersFadeTransition' })(FadeTransitionGroup); diff --git a/packages/material-ui-lab/src/DayPicker/PickersSlideTransition.tsx b/packages/material-ui-lab/src/DayPicker/PickersSlideTransition.tsx new file mode 100644 index 00000000000000..f83ac0e7c6f3a8 --- /dev/null +++ b/packages/material-ui-lab/src/DayPicker/PickersSlideTransition.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { createStyles, WithStyles, withStyles, Theme } from '@material-ui/core/styles'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import { CSSTransitionProps } from 'react-transition-group/CSSTransition'; + +export type SlideDirection = 'right' | 'left'; +export interface SlideTransitionProps extends Omit { + transKey: React.Key; + className?: string; + reduceAnimations: boolean; + slideDirection: SlideDirection; + children: React.ReactElement; +} + +export const slideAnimationDuration = 350; +export const styles = (theme: Theme) => { + const slideTransition = theme.transitions.create('transform', { + duration: slideAnimationDuration, + easing: 'cubic-bezier(0.35, 0.8, 0.4, 1)', + }); + + return createStyles({ + root: { + display: 'block', + position: 'relative', + overflowX: 'hidden', + '& > *': { + position: 'absolute', + top: 0, + right: 0, + left: 0, + }, + }, + 'slideEnter-left': { + willChange: 'transform', + transform: 'translate(100%)', + zIndex: 1, + }, + 'slideEnter-right': { + willChange: 'transform', + transform: 'translate(-100%)', + zIndex: 1, + }, + slideEnterActive: { + transform: 'translate(0%)', + transition: slideTransition, + }, + slideExit: { + transform: 'translate(0%)', + }, + 'slideExitActiveLeft-left': { + willChange: 'transform', + transform: 'translate(-100%)', + transition: slideTransition, + zIndex: 0, + }, + 'slideExitActiveLeft-right': { + willChange: 'transform', + transform: 'translate(100%)', + transition: slideTransition, + zIndex: 0, + }, + }); +}; + +export type PickersSlideTransitionClassKey = keyof WithStyles['classes']; + +/** + * @ignore - do not document. + */ +const SlideTransition: React.FC> = ({ + children, + classes, + className, + reduceAnimations, + slideDirection, + transKey, + ...other +}) => { + if (reduceAnimations) { + return
{children}
; + } + + const transitionClasses = { + exit: classes.slideExit, + enterActive: classes.slideEnterActive, + enter: classes[`slideEnter-${slideDirection}` as 'slideEnter-left' | 'slideEnter-right'], + exitActive: + classes[ + `slideExitActiveLeft-${slideDirection}` as + | 'slideExitActiveLeft-left' + | 'slideExitActiveLeft-right' + ], + }; + + return ( + + React.cloneElement(element, { + classNames: transitionClasses, + }) + } + > + + {children} + + + ); +}; + +export default withStyles(styles, { name: 'MuiPickersSlideTransition' })(SlideTransition); diff --git a/packages/material-ui-lab/src/DayPicker/index.ts b/packages/material-ui-lab/src/DayPicker/index.ts new file mode 100644 index 00000000000000..8bcf99014cb200 --- /dev/null +++ b/packages/material-ui-lab/src/DayPicker/index.ts @@ -0,0 +1,4 @@ +export { default } from './DayPicker'; + +export type DayPickerClassKey = import('./DayPicker').DayPickerClassKey; +export type DayPickerProps = import('./DayPicker').DayPickerProps; diff --git a/packages/pickers/lib/src/views/Calendar/useCalendarState.tsx b/packages/material-ui-lab/src/DayPicker/useCalendarState.tsx similarity index 84% rename from packages/pickers/lib/src/views/Calendar/useCalendarState.tsx rename to packages/material-ui-lab/src/DayPicker/useCalendarState.tsx index 7cc3a294b43a90..7704b58c4e8b81 100644 --- a/packages/pickers/lib/src/views/Calendar/useCalendarState.tsx +++ b/packages/material-ui-lab/src/DayPicker/useCalendarState.tsx @@ -1,13 +1,12 @@ import * as React from 'react'; -import { CalendarViewProps } from './CalendarView'; -import { SlideDirection } from './SlideTransition'; -import { validateDate } from '../../_helpers/date-utils'; -import { MuiPickersAdapter, useUtils, useNow } from '../../_shared/hooks/useUtils'; +import { SlideDirection } from './PickersSlideTransition'; +import { validateDate } from '../internal/pickers/date-utils'; +import { MuiPickersAdapter, useUtils, useNow } from '../internal/pickers/hooks/useUtils'; interface CalendarState { isMonthSwitchingAnimating: boolean; currentMonth: TDate; - focusedDay: TDate; + focusedDay: TDate | null; slideDirection: SlideDirection; } @@ -21,13 +20,13 @@ interface ChangeMonthPayload { export const createCalendarStateReducer = ( reduceAnimations: boolean, disableSwitchToMonthOnDayFocus: boolean, - utils: MuiPickersAdapter + utils: MuiPickersAdapter, ) => ( state: CalendarState, action: | ReducerAction<'finishMonthSwitchingAnimation'> | ReducerAction<'changeMonth', ChangeMonthPayload> - | ReducerAction<'changeFocusedDay', { focusedDay: TDate }> + | ReducerAction<'changeFocusedDay', { focusedDay: TDate }>, ): CalendarState => { switch (action.type) { case 'changeMonth': @@ -65,21 +64,23 @@ export const createCalendarStateReducer = ( }; type CalendarStateInput = Pick< - CalendarViewProps, + import('./DayPicker').DayPickerProps, | 'disableFuture' | 'disablePast' | 'shouldDisableDate' | 'date' | 'reduceAnimations' | 'onMonthChange' + | 'defaultCalendarMonth' + | 'minDate' + | 'maxDate' > & { - minDate: TDate; - maxDate: TDate; disableSwitchToMonthOnDayFocus?: boolean; }; export function useCalendarState({ date, + defaultCalendarMonth, disableFuture, disablePast, disableSwitchToMonthOnDayFocus = false, @@ -91,15 +92,15 @@ export function useCalendarState({ }: CalendarStateInput) { const now = useNow(); const utils = useUtils(); - const dateForMonth = date || now; + const reducerFn = React.useRef( - createCalendarStateReducer(Boolean(reduceAnimations), disableSwitchToMonthOnDayFocus, utils) + createCalendarStateReducer(Boolean(reduceAnimations), disableSwitchToMonthOnDayFocus, utils), ).current; const [calendarState, dispatch] = React.useReducer(reducerFn, { isMonthSwitchingAnimating: false, focusedDay: date, - currentMonth: utils.startOfMonth(dateForMonth), + currentMonth: utils.startOfMonth(date ?? defaultCalendarMonth ?? now), slideDirection: 'left', }); @@ -114,7 +115,7 @@ export function useCalendarState({ onMonthChange(payload.newMonth); } }, - [onMonthChange] + [onMonthChange], ); const changeMonth = React.useCallback( @@ -131,7 +132,7 @@ export function useCalendarState({ : 'right', }); }, - [calendarState.currentMonth, handleChangeMonth, now, utils] + [calendarState.currentMonth, handleChangeMonth, now, utils], ); const isDateDisabled = React.useCallback( @@ -143,7 +144,7 @@ export function useCalendarState({ maxDate, shouldDisableDate, }) !== null, - [disableFuture, disablePast, maxDate, minDate, shouldDisableDate, utils] + [disableFuture, disablePast, maxDate, minDate, shouldDisableDate, utils], ); const onMonthSwitchingAnimationEnd = React.useCallback(() => { @@ -156,7 +157,7 @@ export function useCalendarState({ dispatch({ type: 'changeFocusedDay', focusedDay: newFocusedDate }); } }, - [isDateDisabled] + [isDateDisabled], ); return { diff --git a/packages/material-ui-lab/src/DesktopDatePicker/DesktopDatePicker.tsx b/packages/material-ui-lab/src/DesktopDatePicker/DesktopDatePicker.tsx new file mode 100644 index 00000000000000..79766310bbf9ca --- /dev/null +++ b/packages/material-ui-lab/src/DesktopDatePicker/DesktopDatePicker.tsx @@ -0,0 +1,216 @@ +import PropTypes from 'prop-types'; +import { + datePickerConfig, + DatePickerGenericComponent, + BaseDatePickerProps, +} from '../DatePicker/DatePicker'; +import { DesktopWrapper } from '../internal/pickers/wrappers/Wrapper'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const DesktopDatePicker = makePickerWithStateAndWrapper>( + DesktopWrapper, + { + name: 'MuiDesktopDatePicker', + ...datePickerConfig, + }, +) as DatePickerGenericComponent; + +(DesktopDatePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Popper props passed down to [Popper](https://material-ui.com/api/popper/) component. + */ + PopperProps: PropTypes.object, + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * Custom component for popper [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). + */ + TransitionComponent: PropTypes.elementType, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), +}; + +export type DesktopDatePickerProps = React.ComponentProps; + +export default DesktopDatePicker; diff --git a/packages/material-ui-lab/src/DesktopDatePicker/index.ts b/packages/material-ui-lab/src/DesktopDatePicker/index.ts new file mode 100644 index 00000000000000..27bff9dadd55ef --- /dev/null +++ b/packages/material-ui-lab/src/DesktopDatePicker/index.ts @@ -0,0 +1,2 @@ +export * from './DesktopDatePicker'; +export { default } from './DesktopDatePicker'; diff --git a/packages/material-ui-lab/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx b/packages/material-ui-lab/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx new file mode 100644 index 00000000000000..6650517ca45471 --- /dev/null +++ b/packages/material-ui-lab/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx @@ -0,0 +1,335 @@ +import PropTypes from 'prop-types'; +import { makeDateRangePicker } from '../DateRangePicker/makeDateRangePicker'; +import DesktopTooltipWrapper from '../internal/pickers/wrappers/DesktopTooltipWrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const DesktopDateRangePicker = makeDateRangePicker( + 'MuiPickersDateRangePicker', + DesktopTooltipWrapper, +); + +(DesktopDateRangePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * If `true`, `onChange` is fired on click even if the same date is selected. + * @default false + */ + allowSameDateSelection: PropTypes.bool, + /** + * The number of calendars that render on **desktop**. + * @default 2 + */ + calendars: PropTypes.oneOf([1, 2, 3]), + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * if `true` after selecting `start` date calendar will not automatically switch to the month of `end` date + * @default false + */ + disableAutoMonthSwitching: PropTypes.bool, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * If `true`, todays date is rendering without highlighting with circle. + * @default false + */ + disableHighlightToday: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Text for end input label and toolbar placeholder. + * @default "end" + */ + endText: PropTypes.node, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * Get aria-label text for switching between views button. + */ + getViewSwitchingButtonText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * Max selectable date. @DateIOType + * @default Date(2099-31-12) + */ + maxDate: PropTypes.any, + /** + * Min selectable date. @DateIOType + * @default Date(1900-01-01) + */ + minDate: PropTypes.any, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Popper props passed down to [Popper](https://material-ui.com/api/popper/) component. + */ + PopperProps: PropTypes.object, + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Custom renderer for `` days. @DateIOType + * @example (date, DateRangeDayProps) => + */ + renderDay: PropTypes.func, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `startProps` and `endProps` arguments of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api), + * that you need to forward to the range start/end inputs respectively. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example + * ```jsx + * ( + * + * + * to + * + * ; + * )} + * /> + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. + * @default false + */ + showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Text for start input label and toolbar placeholder. + * @default "Start" + */ + startText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * Custom component for popper [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). + */ + TransitionComponent: PropTypes.elementType, + /** + * The value of the picker. + */ + value: PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + ).isRequired, +}; + +export type DesktopDateRangePickerProps = React.ComponentProps; + +export type DateRange = import('../DateRangePicker/RangeTypes').DateRange; + +export default DesktopDateRangePicker; diff --git a/packages/material-ui-lab/src/DesktopDateRangePicker/index.ts b/packages/material-ui-lab/src/DesktopDateRangePicker/index.ts new file mode 100644 index 00000000000000..70a217d0a9ab86 --- /dev/null +++ b/packages/material-ui-lab/src/DesktopDateRangePicker/index.ts @@ -0,0 +1,2 @@ +export * from './DesktopDateRangePicker'; +export { default } from './DesktopDateRangePicker'; diff --git a/packages/material-ui-lab/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx b/packages/material-ui-lab/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx new file mode 100644 index 00000000000000..f5a78b963a9e53 --- /dev/null +++ b/packages/material-ui-lab/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx @@ -0,0 +1,408 @@ +import PropTypes from 'prop-types'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; +import { + BaseDateTimePickerProps, + dateTimePickerConfig, + DateTimePickerGenericComponent, +} from '../DateTimePicker/DateTimePicker'; +import { DesktopWrapper } from '../internal/pickers/wrappers/Wrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const DesktopDateTimePicker = makePickerWithStateAndWrapper>( + DesktopWrapper, + { + name: 'MuiDesktopDateTimePicker', + ...dateTimePickerConfig, + }, +) as DateTimePickerGenericComponent; + +(DesktopDateTimePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * If `true`, `onChange` is fired on click even if the same date is selected. + * @default false + */ + allowSameDateSelection: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ + ampm: PropTypes.bool, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Date tab icon. + */ + dateRangeIcon: PropTypes.node, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * If `true`, todays date is rendering without highlighting with circle. + * @default false + */ + disableHighlightToday: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * Get aria-label text for switching between views button. + */ + getViewSwitchingButtonText: PropTypes.func, + /** + * To show tabs. + */ + hideTabs: PropTypes.bool, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Minimal selectable moment of time with binding to date, to set max time in each day use `maxTime`. + */ + maxDateTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + maxTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Minimal selectable moment of time with binding to date, to set min time in each day use `minTime`. + */ + minDateTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Callback firing on year change @DateIOType. + */ + onYearChange: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * First view to show. + */ + openTo: PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'seconds', 'year']), + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Popper props passed down to [Popper](https://material-ui.com/api/popper/) component. + */ + PopperProps: PropTypes.object, + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Custom renderer for day. Check [DayComponentProps api](https://material-ui-pickers.dev/api/Day) @DateIOType. + */ + renderDay: PropTypes.func, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. + * @default false + */ + showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Time tab icon. + */ + timeIcon: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * Custom component for popper [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). + */ + TransitionComponent: PropTypes.elementType, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Array of views to show. + */ + views: PropTypes.arrayOf( + PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'year']).isRequired, + ), +}; + +export type DesktopDateTimePickerProps = React.ComponentProps; + +export default DesktopDateTimePicker; diff --git a/packages/material-ui-lab/src/DesktopDateTimePicker/index.ts b/packages/material-ui-lab/src/DesktopDateTimePicker/index.ts new file mode 100644 index 00000000000000..dd933ec258a6e7 --- /dev/null +++ b/packages/material-ui-lab/src/DesktopDateTimePicker/index.ts @@ -0,0 +1,2 @@ +export * from './DesktopDateTimePicker'; +export { default } from './DesktopDateTimePicker'; diff --git a/packages/material-ui-lab/src/DesktopTimePicker/DesktopTimePicker.tsx b/packages/material-ui-lab/src/DesktopTimePicker/DesktopTimePicker.tsx new file mode 100644 index 00000000000000..c795fe62f09599 --- /dev/null +++ b/packages/material-ui-lab/src/DesktopTimePicker/DesktopTimePicker.tsx @@ -0,0 +1,256 @@ +import PropTypes from 'prop-types'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; +import { + BaseTimePickerProps, + timePickerConfig, + TimePickerGenericComponent, +} from '../TimePicker/TimePicker'; +import { DesktopWrapper } from '../internal/pickers/wrappers/Wrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const DesktopTimePicker = makePickerWithStateAndWrapper(DesktopWrapper, { + name: 'MuiDesktopTimePicker', + ...timePickerConfig, +}) as TimePickerGenericComponent; + +(DesktopTimePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ + ampm: PropTypes.bool, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * First view to show. + */ + openTo: PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'seconds', 'year']), + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Popper props passed down to [Popper](https://material-ui.com/api/popper/) component. + */ + PopperProps: PropTypes.object, + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * Custom component for popper [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). + */ + TransitionComponent: PropTypes.elementType, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Array of views to show. + */ + views: PropTypes.arrayOf(PropTypes.oneOf(['hours', 'minutes', 'seconds']).isRequired), +}; + +export type DesktopTimePickerProps = React.ComponentProps; + +export default DesktopTimePicker; diff --git a/packages/material-ui-lab/src/DesktopTimePicker/index.ts b/packages/material-ui-lab/src/DesktopTimePicker/index.ts new file mode 100644 index 00000000000000..49e5d11a63b727 --- /dev/null +++ b/packages/material-ui-lab/src/DesktopTimePicker/index.ts @@ -0,0 +1,2 @@ +export { default } from './DesktopTimePicker'; +export * from './DesktopTimePicker'; diff --git a/packages/material-ui-lab/src/LocalizationProvider/LocalizationProvider.tsx b/packages/material-ui-lab/src/LocalizationProvider/LocalizationProvider.tsx new file mode 100644 index 00000000000000..d4446d2c997d2f --- /dev/null +++ b/packages/material-ui-lab/src/LocalizationProvider/LocalizationProvider.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { DateIOFormats, IUtils } from '@date-io/core/IUtils'; + +export type MuiPickersAdapter = IUtils; + +export const MuiPickersAdapterContext = React.createContext(null); + +export interface LocalizationProviderProps { + children?: React.ReactNode; + /** DateIO adapter class function */ + dateAdapter: new (...args: any) => MuiPickersAdapter; + /** Formats that are used for any child pickers */ + dateFormats?: Partial; + /** + * Date library instance you are using, if it has some global overrides + * ```jsx + * dateLibInstance={momentTimeZone} + * ``` + */ + dateLibInstance?: any; + /** Locale for the date library you are using */ + locale?: string | object; +} + +/** + * @ignore - do not document. + */ +const LocalizationProvider: React.FC = (props) => { + const { children, dateAdapter: Utils, dateFormats, dateLibInstance, locale } = props; + const utils = React.useMemo( + () => new Utils({ locale, formats: dateFormats, instance: dateLibInstance }), + [Utils, locale, dateFormats, dateLibInstance], + ); + + return ( + {children} + ); +}; + +(LocalizationProvider as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, + /** + * DateIO adapter class function + */ + dateAdapter: PropTypes.func.isRequired, + /** + * Formats that are used for any child pickers + */ + dateFormats: PropTypes.shape({ + dayOfMonth: PropTypes.string, + fullDate: PropTypes.string, + fullDateTime: PropTypes.string, + fullDateTime12h: PropTypes.string, + fullDateTime24h: PropTypes.string, + fullDateWithWeekday: PropTypes.string, + fullTime: PropTypes.string, + fullTime12h: PropTypes.string, + fullTime24h: PropTypes.string, + hours12h: PropTypes.string, + hours24h: PropTypes.string, + keyboardDate: PropTypes.string, + keyboardDateTime: PropTypes.string, + keyboardDateTime12h: PropTypes.string, + keyboardDateTime24h: PropTypes.string, + minutes: PropTypes.string, + month: PropTypes.string, + monthAndDate: PropTypes.string, + monthAndYear: PropTypes.string, + monthShort: PropTypes.string, + normalDate: PropTypes.string, + normalDateWithWeekday: PropTypes.string, + seconds: PropTypes.string, + shortDate: PropTypes.string, + weekday: PropTypes.string, + weekdayShort: PropTypes.string, + year: PropTypes.string, + }), + /** + * Date library instance you are using, if it has some global overrides + * ```jsx + * dateLibInstance={momentTimeZone} + * ``` + */ + dateLibInstance: PropTypes.any, + /** + * Locale for the date library you are using + */ + locale: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), +}; + +export default LocalizationProvider; diff --git a/packages/material-ui-lab/src/LocalizationProvider/index.ts b/packages/material-ui-lab/src/LocalizationProvider/index.ts new file mode 100644 index 00000000000000..09d5ca51f422fb --- /dev/null +++ b/packages/material-ui-lab/src/LocalizationProvider/index.ts @@ -0,0 +1,2 @@ +export * from './LocalizationProvider'; +export { default } from './LocalizationProvider'; diff --git a/packages/material-ui-lab/src/MobileDatePicker/MobileDatePicker.tsx b/packages/material-ui-lab/src/MobileDatePicker/MobileDatePicker.tsx new file mode 100644 index 00000000000000..679eaa31c21315 --- /dev/null +++ b/packages/material-ui-lab/src/MobileDatePicker/MobileDatePicker.tsx @@ -0,0 +1,242 @@ +import PropTypes from 'prop-types'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; +import { + BaseDatePickerProps, + datePickerConfig, + DatePickerGenericComponent, +} from '../DatePicker/DatePicker'; +import { MobileWrapper } from '../internal/pickers/wrappers/Wrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const MobileDatePicker = makePickerWithStateAndWrapper>( + MobileWrapper, + { + name: 'MuiMobileDatePicker', + ...datePickerConfig, + }, +) as DatePickerGenericComponent; + +(MobileDatePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * "CANCEL" Text message + * @default "CANCEL" + */ + cancelText: PropTypes.node, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * If `true`, it shows the clear action in the picker dialog. + * @default false + */ + clearable: PropTypes.bool, + /** + * "CLEAR" Text message + * @default "CLEAR" + */ + clearText: PropTypes.node, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * "OK" button text. + * @default "OK" + */ + okText: PropTypes.node, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. + * @default false + */ + showTodayButton: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * "TODAY" Text message + * @default "TODAY" + */ + todayText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), +}; + +export type MobileDatePickerProps = React.ComponentProps; + +export default MobileDatePicker; diff --git a/packages/material-ui-lab/src/MobileDatePicker/index.ts b/packages/material-ui-lab/src/MobileDatePicker/index.ts new file mode 100644 index 00000000000000..806f5f2e64f390 --- /dev/null +++ b/packages/material-ui-lab/src/MobileDatePicker/index.ts @@ -0,0 +1,2 @@ +export * from './MobileDatePicker'; +export { default } from './MobileDatePicker'; diff --git a/packages/material-ui-lab/src/MobileDateRangePicker/MobileDateRangePicker.tsx b/packages/material-ui-lab/src/MobileDateRangePicker/MobileDateRangePicker.tsx new file mode 100644 index 00000000000000..d1e0d286814d20 --- /dev/null +++ b/packages/material-ui-lab/src/MobileDateRangePicker/MobileDateRangePicker.tsx @@ -0,0 +1,358 @@ +import PropTypes from 'prop-types'; +import { makeDateRangePicker } from '../DateRangePicker/makeDateRangePicker'; +import MobileWrapper from '../internal/pickers/wrappers/MobileWrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const MobileDateRangePicker = makeDateRangePicker('MuiPickersDateRangePicker', MobileWrapper); + +(MobileDateRangePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * If `true`, `onChange` is fired on click even if the same date is selected. + * @default false + */ + allowSameDateSelection: PropTypes.bool, + /** + * The number of calendars that render on **desktop**. + * @default 2 + */ + calendars: PropTypes.oneOf([1, 2, 3]), + /** + * "CANCEL" Text message + * @default "CANCEL" + */ + cancelText: PropTypes.node, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * If `true`, it shows the clear action in the picker dialog. + * @default false + */ + clearable: PropTypes.bool, + /** + * "CLEAR" Text message + * @default "CLEAR" + */ + clearText: PropTypes.node, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps: PropTypes.object, + /** + * if `true` after selecting `start` date calendar will not automatically switch to the month of `end` date + * @default false + */ + disableAutoMonthSwitching: PropTypes.bool, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * If `true`, todays date is rendering without highlighting with circle. + * @default false + */ + disableHighlightToday: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Text for end input label and toolbar placeholder. + * @default "end" + */ + endText: PropTypes.node, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * Get aria-label text for switching between views button. + */ + getViewSwitchingButtonText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * Max selectable date. @DateIOType + * @default Date(2099-31-12) + */ + maxDate: PropTypes.any, + /** + * Min selectable date. @DateIOType + * @default Date(1900-01-01) + */ + minDate: PropTypes.any, + /** + * "OK" button text. + * @default "OK" + */ + okText: PropTypes.node, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Custom renderer for `` days. @DateIOType + * @example (date, DateRangeDayProps) => + */ + renderDay: PropTypes.func, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `startProps` and `endProps` arguments of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api), + * that you need to forward to the range start/end inputs respectively. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example + * ```jsx + * ( + * + * + * to + * + * ; + * )} + * /> + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. + * @default false + */ + showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. + * @default false + */ + showTodayButton: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Text for start input label and toolbar placeholder. + * @default "Start" + */ + startText: PropTypes.node, + /** + * "TODAY" Text message + * @default "TODAY" + */ + todayText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + ).isRequired, +}; + +export type MobileDateRangePickerProps = React.ComponentProps; + +export type DateRange = import('../DateRangePicker/RangeTypes').DateRange; + +export default MobileDateRangePicker; diff --git a/packages/material-ui-lab/src/MobileDateRangePicker/index.ts b/packages/material-ui-lab/src/MobileDateRangePicker/index.ts new file mode 100644 index 00000000000000..85184d22ee3d98 --- /dev/null +++ b/packages/material-ui-lab/src/MobileDateRangePicker/index.ts @@ -0,0 +1,2 @@ +export * from './MobileDateRangePicker'; +export { default } from './MobileDateRangePicker'; diff --git a/packages/material-ui-lab/src/MobileDateTimePicker/MobileDateTimePicker.tsx b/packages/material-ui-lab/src/MobileDateTimePicker/MobileDateTimePicker.tsx new file mode 100644 index 00000000000000..8f4e9aa91e0e98 --- /dev/null +++ b/packages/material-ui-lab/src/MobileDateTimePicker/MobileDateTimePicker.tsx @@ -0,0 +1,434 @@ +import PropTypes from 'prop-types'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; +import { + BaseDateTimePickerProps, + dateTimePickerConfig, + DateTimePickerGenericComponent, +} from '../DateTimePicker/DateTimePicker'; +import { MobileWrapper } from '../internal/pickers/wrappers/Wrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const MobileDateTimePicker = makePickerWithStateAndWrapper>( + MobileWrapper, + { + name: 'MuiMobileDateTimePicker', + ...dateTimePickerConfig, + }, +) as DateTimePickerGenericComponent; + +(MobileDateTimePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * If `true`, `onChange` is fired on click even if the same date is selected. + * @default false + */ + allowSameDateSelection: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ + ampm: PropTypes.bool, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * "CANCEL" Text message + * @default "CANCEL" + */ + cancelText: PropTypes.node, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * If `true`, it shows the clear action in the picker dialog. + * @default false + */ + clearable: PropTypes.bool, + /** + * "CLEAR" Text message + * @default "CLEAR" + */ + clearText: PropTypes.node, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Date tab icon. + */ + dateRangeIcon: PropTypes.node, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * If `true`, todays date is rendering without highlighting with circle. + * @default false + */ + disableHighlightToday: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * Get aria-label text for switching between views button. + */ + getViewSwitchingButtonText: PropTypes.func, + /** + * To show tabs. + */ + hideTabs: PropTypes.bool, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Minimal selectable moment of time with binding to date, to set max time in each day use `maxTime`. + */ + maxDateTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + maxTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Minimal selectable moment of time with binding to date, to set min time in each day use `minTime`. + */ + minDateTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * "OK" button text. + * @default "OK" + */ + okText: PropTypes.node, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Callback firing on year change @DateIOType. + */ + onYearChange: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * First view to show. + */ + openTo: PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'seconds', 'year']), + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Custom renderer for day. Check [DayComponentProps api](https://material-ui-pickers.dev/api/Day) @DateIOType. + */ + renderDay: PropTypes.func, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. + * @default false + */ + showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. + * @default false + */ + showTodayButton: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Time tab icon. + */ + timeIcon: PropTypes.node, + /** + * "TODAY" Text message + * @default "TODAY" + */ + todayText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Array of views to show. + */ + views: PropTypes.arrayOf( + PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'year']).isRequired, + ), +}; + +export type MobileDateTimePickerProps = React.ComponentProps; + +export default MobileDateTimePicker; diff --git a/packages/material-ui-lab/src/MobileDateTimePicker/index.ts b/packages/material-ui-lab/src/MobileDateTimePicker/index.ts new file mode 100644 index 00000000000000..5b8406580eeb54 --- /dev/null +++ b/packages/material-ui-lab/src/MobileDateTimePicker/index.ts @@ -0,0 +1,2 @@ +export * from './MobileDateTimePicker'; +export { default } from './MobileDateTimePicker'; diff --git a/packages/material-ui-lab/src/MobileTimePicker/MobileTimePicker.tsx b/packages/material-ui-lab/src/MobileTimePicker/MobileTimePicker.tsx new file mode 100644 index 00000000000000..ef2682fe228f3b --- /dev/null +++ b/packages/material-ui-lab/src/MobileTimePicker/MobileTimePicker.tsx @@ -0,0 +1,282 @@ +import PropTypes from 'prop-types'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; +import { + BaseTimePickerProps, + timePickerConfig, + TimePickerGenericComponent, +} from '../TimePicker/TimePicker'; +import { MobileWrapper } from '../internal/pickers/wrappers/Wrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const MobileTimePicker = makePickerWithStateAndWrapper(MobileWrapper, { + name: 'MuiMobileTimePicker', + ...timePickerConfig, +}) as TimePickerGenericComponent; + +(MobileTimePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ + ampm: PropTypes.bool, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * "CANCEL" Text message + * @default "CANCEL" + */ + cancelText: PropTypes.node, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * If `true`, it shows the clear action in the picker dialog. + * @default false + */ + clearable: PropTypes.bool, + /** + * "CLEAR" Text message + * @default "CLEAR" + */ + clearText: PropTypes.node, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * "OK" button text. + * @default "OK" + */ + okText: PropTypes.node, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * First view to show. + */ + openTo: PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'seconds', 'year']), + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. + * @default false + */ + showTodayButton: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * "TODAY" Text message + * @default "TODAY" + */ + todayText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Array of views to show. + */ + views: PropTypes.arrayOf(PropTypes.oneOf(['hours', 'minutes', 'seconds']).isRequired), +}; + +export type MobileTimePickerProps = React.ComponentProps; + +export default MobileTimePicker; diff --git a/packages/material-ui-lab/src/MobileTimePicker/index.ts b/packages/material-ui-lab/src/MobileTimePicker/index.ts new file mode 100644 index 00000000000000..7ad8bb56683686 --- /dev/null +++ b/packages/material-ui-lab/src/MobileTimePicker/index.ts @@ -0,0 +1,2 @@ +export * from './MobileTimePicker'; +export { default } from './MobileTimePicker'; diff --git a/packages/material-ui-lab/src/MonthPicker/MonthPicker.test.tsx b/packages/material-ui-lab/src/MonthPicker/MonthPicker.test.tsx new file mode 100644 index 00000000000000..12e39968181ca4 --- /dev/null +++ b/packages/material-ui-lab/src/MonthPicker/MonthPicker.test.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { spy } from 'sinon'; +import { expect } from 'chai'; +import { getClasses, createMount, fireEvent, screen, describeConformance } from 'test/utils'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import MonthPicker from '@material-ui/lab/MonthPicker'; +import { createPickerRender } from '../internal/pickers/test-utils'; + +describe('', () => { + const mount = createMount(); + const render = createPickerRender({ strict: false }); + let classes: Record; + + const localizedMount = (node: React.ReactNode) => { + return mount({node}); + }; + + before(() => { + classes = getClasses( + {}} + />, + ); + }); + + describeConformance( + {}} + />, + () => ({ + classes, + inheritComponent: 'div', + mount: localizedMount, + refInstanceof: window.HTMLDivElement, + // cannot test reactTestRenderer because of required context + skip: ['componentProp', 'propsSpread', 'reactTestRenderer'], + }), + ); + + it('allows to pick year standalone', () => { + const onChangeMock = spy(); + render( + , + ); + + fireEvent.click(screen.getByText('May', { selector: 'button' })); + expect((onChangeMock.args[0][0] as Date).getMonth()).to.equal(4); // month index starting from 0 + }); +}); diff --git a/packages/material-ui-lab/src/MonthPicker/MonthPicker.tsx b/packages/material-ui-lab/src/MonthPicker/MonthPicker.tsx new file mode 100644 index 00000000000000..97e7c2ecf1bb2d --- /dev/null +++ b/packages/material-ui-lab/src/MonthPicker/MonthPicker.tsx @@ -0,0 +1,151 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import PickersMonth from './PickersMonth'; +import { useUtils, useNow } from '../internal/pickers/hooks/useUtils'; +import { PickerOnChangeFn } from '../internal/pickers/hooks/useViews'; + +export interface MonthPickerProps { + /** Date value for the MonthPicker */ + date: TDate | null; + /** Minimal selectable date. */ + minDate: TDate; + /** Maximal selectable date. */ + maxDate: TDate; + /** Callback fired on date change. */ + onChange: PickerOnChangeFn; + /** If `true` past days are disabled. */ + disablePast?: boolean | null; + /** If `true` future days are disabled. */ + disableFuture?: boolean | null; + className?: string; + onMonthChange?: (date: TDate) => void | Promise; +} + +export const styles = createStyles({ + root: { + width: 310, + display: 'flex', + flexWrap: 'wrap', + alignContent: 'stretch', + }, +}); + +export type MonthPickerClassKey = keyof WithStyles['classes']; + +/** + * @ignore - do not document. + */ +const MonthPicker = React.forwardRef(function MonthPicker( + props: MonthPickerProps & WithStyles, + ref: React.Ref, +) { + const { + className, + classes, + date, + disableFuture, + disablePast, + maxDate, + minDate, + onChange, + onMonthChange, + } = props; + + const utils = useUtils(); + const now = useNow(); + const currentMonth = utils.getMonth(date || now); + + const shouldDisableMonth = (month: TDate) => { + const firstEnabledMonth = utils.startOfMonth( + disablePast && utils.isAfter(now, minDate) ? now : minDate, + ); + + const lastEnabledMonth = utils.startOfMonth( + disableFuture && utils.isBefore(now, maxDate) ? now : maxDate, + ); + + const isBeforeFirstEnabled = utils.isBefore(month, firstEnabledMonth); + const isAfterLastEnabled = utils.isAfter(month, lastEnabledMonth); + + return isBeforeFirstEnabled || isAfterLastEnabled; + }; + + const onMonthSelect = (month: number) => { + const newDate = utils.setMonth(date || now, month); + + onChange(newDate, 'finish'); + if (onMonthChange) { + onMonthChange(newDate); + } + }; + + return ( +
+ {utils.getMonthArray(date || now).map((month) => { + const monthNumber = utils.getMonth(month); + const monthText = utils.format(month, 'monthShort'); + + return ( + + {monthText} + + ); + })} +
+ ); +}); + +(MonthPicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, + /** + * Date value for the MonthPicker + */ + date: PropTypes.any, + /** + * If `true` future days are disabled. + */ + disableFuture: PropTypes.bool, + /** + * If `true` past days are disabled. + */ + disablePast: PropTypes.bool, + /** + * Maximal selectable date. + */ + maxDate: PropTypes.any.isRequired, + /** + * Minimal selectable date. + */ + minDate: PropTypes.any.isRequired, + /** + * Callback fired on date change. + */ + onChange: PropTypes.func.isRequired, + /** + * @ignore + */ + onMonthChange: PropTypes.func, +}; + +export default withStyles(styles, { name: 'MuiMonthPicker' })(MonthPicker) as ( + props: MonthPickerProps & React.RefAttributes, +) => JSX.Element; diff --git a/packages/pickers/lib/src/views/Calendar/Month.tsx b/packages/material-ui-lab/src/MonthPicker/PickersMonth.tsx similarity index 69% rename from packages/pickers/lib/src/views/Calendar/Month.tsx rename to packages/material-ui-lab/src/MonthPicker/PickersMonth.tsx index 01ba9d74f7d3bd..1ab1454700f6b9 100644 --- a/packages/pickers/lib/src/views/Calendar/Month.tsx +++ b/packages/material-ui-lab/src/MonthPicker/PickersMonth.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import clsx from 'clsx'; import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; -import { onSpaceOrEnter } from '../../_helpers/utils'; +import { createStyles, WithStyles, withStyles, Theme } from '@material-ui/core/styles'; +import { onSpaceOrEnter } from '../internal/pickers/utils'; export interface MonthProps { children: React.ReactNode; @@ -12,8 +12,8 @@ export interface MonthProps { value: any; } -export const useStyles = makeStyles( - (theme) => ({ +export const styles = (theme: Theme) => + createStyles({ root: { flex: '1 0 33.33%', display: 'flex', @@ -37,13 +37,15 @@ export const useStyles = makeStyles( }, }, selected: {}, - }), - { name: 'MuiPickersMonth' } -); + }); -export const Month: React.FC = (props) => { - const { disabled, onSelect, selected, value, ...other } = props; - const classes = useStyles(); +export type PickersMonthClassKey = keyof WithStyles['classes']; + +/** + * @ignore - do not document. + */ +const PickersMonth: React.FC> = (props) => { + const { classes, disabled, onSelect, selected, value, ...other } = props; const handleSelection = () => { onSelect(value); }; @@ -51,8 +53,7 @@ export const Month: React.FC = (props) => { return ( = (props) => { ); }; -Month.displayName = 'Month'; - -export default Month; +export default withStyles(styles, { name: 'MuiPickersMonth' })(PickersMonth); diff --git a/packages/material-ui-lab/src/MonthPicker/index.ts b/packages/material-ui-lab/src/MonthPicker/index.ts new file mode 100644 index 00000000000000..abb37b0aca9426 --- /dev/null +++ b/packages/material-ui-lab/src/MonthPicker/index.ts @@ -0,0 +1,4 @@ +export { default } from './MonthPicker'; + +export type MonthPickerClassKey = import('./MonthPicker').MonthPickerClassKey; +export type MonthPickerProps = import('./MonthPicker').MonthPickerProps; diff --git a/packages/material-ui-lab/src/PickersCalendarSkeleton/PickersCalendarSkeleton.tsx b/packages/material-ui-lab/src/PickersCalendarSkeleton/PickersCalendarSkeleton.tsx new file mode 100644 index 00000000000000..a264649af809c3 --- /dev/null +++ b/packages/material-ui-lab/src/PickersCalendarSkeleton/PickersCalendarSkeleton.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import Skeleton from '@material-ui/core/Skeleton'; +import { createStyles, WithStyles, withStyles, Theme } from '@material-ui/core/styles'; +import { DAY_SIZE, DAY_MARGIN } from '../internal/pickers/constants/dimensions'; +import { styles as calendarStyles } from '../DayPicker/PickersCalendar'; + +export interface PickersCalendarSkeletonProps extends React.HTMLProps {} + +export const styles = (theme: Theme) => + createStyles({ + ...calendarStyles(theme), + root: { + alignSelf: 'start', + }, + daySkeleton: { + margin: `0 ${DAY_MARGIN}px`, + }, + hidden: { + visibility: 'hidden', + }, + }); + +export type PickersCalendarSkeletonClassKey = keyof WithStyles['classes']; + +const monthMap = [ + [0, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 0, 0, 0], +]; + +/** + * @ignore - do not document. + */ +const PickersCalendarSkeleton: React.FC< + PickersCalendarSkeletonProps & WithStyles +> = (props) => { + const { className, classes, ...other } = props; + + return ( +
+ {monthMap.map((week, index) => ( +
+ {week.map((day, index2) => ( + + ))} +
+ ))} +
+ ); +}; + +(PickersCalendarSkeleton as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + children: PropTypes.node, + /** + * @ignore + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, +}; + +export default withStyles(styles, { name: 'MuiCalendarSkeleton' })(PickersCalendarSkeleton); diff --git a/packages/material-ui-lab/src/PickersCalendarSkeleton/index.ts b/packages/material-ui-lab/src/PickersCalendarSkeleton/index.ts new file mode 100644 index 00000000000000..6cae7d8b33c215 --- /dev/null +++ b/packages/material-ui-lab/src/PickersCalendarSkeleton/index.ts @@ -0,0 +1,3 @@ +export * from './PickersCalendarSkeleton'; + +export { default } from './PickersCalendarSkeleton'; diff --git a/packages/material-ui-lab/src/PickersDay/PickersDay.test.tsx b/packages/material-ui-lab/src/PickersDay/PickersDay.test.tsx new file mode 100644 index 00000000000000..e016eb5cfbcba0 --- /dev/null +++ b/packages/material-ui-lab/src/PickersDay/PickersDay.test.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { getClasses, createMount, describeConformance } from 'test/utils'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import PickersDay from '@material-ui/lab/PickersDay'; + +describe('', () => { + const mount = createMount(); + let classes: Record; + + const localizedMount = (node: React.ReactNode) => { + return mount({node}); + }; + + before(() => { + classes = getClasses( + {}} + />, + ); + }); + + describeConformance( + {}} + />, + () => ({ + classes, + inheritComponent: 'button', + mount: localizedMount, + refInstanceof: window.HTMLButtonElement, + // cannot test reactTestRenderer because of required context + skip: ['componentProp', 'reactTestRenderer'], + }), + ); +}); diff --git a/packages/pickers/lib/src/views/Calendar/Day.tsx b/packages/material-ui-lab/src/PickersDay/PickersDay.tsx similarity index 52% rename from packages/pickers/lib/src/views/Calendar/Day.tsx rename to packages/material-ui-lab/src/PickersDay/PickersDay.tsx index e31c31cb0f0670..e74ef05239f06e 100644 --- a/packages/pickers/lib/src/views/Calendar/Day.tsx +++ b/packages/material-ui-lab/src/PickersDay/PickersDay.tsx @@ -1,20 +1,18 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import clsx from 'clsx'; import ButtonBase, { ButtonBaseProps } from '@material-ui/core/ButtonBase'; -import { makeStyles, fade } from '@material-ui/core/styles'; -import { ExtendMui } from '../../typings/helpers'; -import { onSpaceOrEnter } from '../../_helpers/utils'; -import { useUtils } from '../../_shared/hooks/useUtils'; -import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; -import { useDefaultProps } from '../../_shared/withDefaultProps'; -import { useCanAutoFocus } from '../../_shared/hooks/useCanAutoFocus'; -import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; +import { createStyles, WithStyles, withStyles, Theme, alpha } from '@material-ui/core/styles'; +import { useForkRef } from '@material-ui/core'; +import { ExtendMui } from '../internal/pickers/typings/helpers'; +import { onSpaceOrEnter } from '../internal/pickers/utils'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import { DAY_SIZE, DAY_MARGIN } from '../internal/pickers/constants/dimensions'; +import { useCanAutoFocus } from '../internal/pickers/hooks/useCanAutoFocus'; +import { PickerSelectionState } from '../internal/pickers/hooks/usePickerState'; -const muiComponentConfig = { name: 'MuiPickersDay' }; - -export const useStyles = makeStyles( - (theme) => ({ +export const styles = (theme: Theme) => + createStyles({ root: { ...theme.typography.caption, width: DAY_SIZE, @@ -25,10 +23,10 @@ export const useStyles = makeStyles( backgroundColor: theme.palette.background.paper, color: theme.palette.text.primary, '&:hover': { - backgroundColor: fade(theme.palette.action.active, theme.palette.action.hoverOpacity), + backgroundColor: alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), }, '&:focus': { - backgroundColor: fade(theme.palette.action.active, theme.palette.action.hoverOpacity), + backgroundColor: alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), '&$selected': { willChange: 'background-color', backgroundColor: theme.palette.primary.dark, @@ -69,77 +67,78 @@ export const useStyles = makeStyles( }, selected: {}, disabled: {}, - }), - muiComponentConfig -); + }); + +export type PickersDayClassKey = keyof WithStyles['classes']; -export interface DayProps extends ExtendMui { +export interface PickersDayProps extends ExtendMui { /** * The date to show. */ day: TDate; /** - * Is focused by keyboard navigation. + * If `true`, the day element will be focused during the first mount. */ focused?: boolean; /** - * Can be focused by tabbing in. + * If `true`, allows to focus by tabbing. */ focusable?: boolean; /** - * Is day in current month. - */ - inCurrentMonth: boolean; - /** - * Is switching month animation going on right now. + * If `true`, day is outside of month and will be hidden. */ - isAnimating?: boolean; + outsideCurrentMonth: boolean; /** - * Is today? + * If `true`, renders as today date. */ today?: boolean; /** - * Disabled?. + * If `true`, renders as disabled. */ disabled?: boolean; /** - * Selected? + * If `true`, renders as selected. */ selected?: boolean; /** - * Is keyboard control and focus management enabled. + * If `true`, keyboard control and focus management is enabled. */ allowKeyboardControl?: boolean; /** - * Disable margin between days, useful for displaying range of days. + * If `true`, days are rendering without margin. Useful for displaying linked range of days. */ disableMargin?: boolean; /** - * Display disabled dates outside the current month. - * + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. * @default false */ showDaysOutsideCurrentMonth?: boolean; /** - * Disable highlighting today date with a circle. - * + * If `true`, todays date is rendering without highlighting with circle. * @default false */ disableHighlightToday?: boolean; /** - * Allow selecting the same date (fire onChange even if same date is selected). - * + * If `true`, `onChange` is fired on click even if the same date is selected. * @default false */ allowSameDateSelection?: boolean; + isAnimating?: boolean; onDayFocus?: (day: TDate) => void; onDaySelect: (day: TDate, isFinish: PickerSelectionState) => void; } -function PureDay(props: DayProps) { +/** + * @ignore - do not document. + */ +const PickersDay = React.forwardRef(function PickersDay( + props: PickersDayProps & WithStyles, + forwardedRef: React.Ref, +) { const { allowKeyboardControl, allowSameDateSelection = false, + classes, className, day, disabled = false, @@ -148,37 +147,37 @@ function PureDay(props: DayProps) { focusable = false, focused = false, hidden, - inCurrentMonth: isInCurrentMonth, isAnimating, onClick, onDayFocus, onDaySelect, onFocus, onKeyDown, + outsideCurrentMonth, selected = false, showDaysOutsideCurrentMonth = false, today: isToday = false, ...other - } = useDefaultProps(props, muiComponentConfig); + } = props; const utils = useUtils(); - const classes = useStyles(); const canAutoFocus = useCanAutoFocus(); const ref = React.useRef(null); + const handleRef = useForkRef(ref, forwardedRef); React.useEffect(() => { if ( focused && !disabled && !isAnimating && - isInCurrentMonth && + !outsideCurrentMonth && ref.current && allowKeyboardControl && canAutoFocus ) { ref.current.focus(); } - }, [allowKeyboardControl, canAutoFocus, disabled, focused, isAnimating, isInCurrentMonth]); + }, [allowKeyboardControl, canAutoFocus, disabled, focused, isAnimating, outsideCurrentMonth]); const handleFocus = (event: React.FocusEvent) => { if (!focused && onDayFocus) { @@ -214,36 +213,38 @@ function PureDay(props: DayProps) { [classes.selected]: selected, [classes.dayWithMargin]: !disableMargin, [classes.today]: !disableHighlightToday && isToday, - [classes.dayOutsideMonth]: !isInCurrentMonth && showDaysOutsideCurrentMonth, + [classes.dayOutsideMonth]: outsideCurrentMonth && showDaysOutsideCurrentMonth, }, - className + className, ); - if (!isInCurrentMonth && !showDaysOutsideCurrentMonth) { - // Do not render button and not attach any listeners for empty days + if (outsideCurrentMonth && !showDaysOutsideCurrentMonth) { return
; } return ( {utils.format(day, 'dayOfMonth')} ); -} +}); -export const areDayPropsEqual = (prevProps: DayProps, nextProps: DayProps) => { +export const areDayPropsEqual = ( + prevProps: PickersDayProps, + nextProps: PickersDayProps, +) => { return ( prevProps.focused === nextProps.focused && prevProps.focusable === nextProps.focusable && @@ -256,19 +257,110 @@ export const areDayPropsEqual = (prevProps: DayProps, nextProps: DayProps(props: PickersDayProps & React.RefAttributes) => JSX.Element; diff --git a/packages/material-ui-lab/src/PickersDay/index.ts b/packages/material-ui-lab/src/PickersDay/index.ts new file mode 100644 index 00000000000000..3eb1b8d27e818b --- /dev/null +++ b/packages/material-ui-lab/src/PickersDay/index.ts @@ -0,0 +1,4 @@ +export { default } from './PickersDay'; + +export type PickersDayClassKey = import('./PickersDay').PickersDayClassKey; +export type PickersDayProps = import('./PickersDay').PickersDayProps; diff --git a/packages/material-ui-lab/src/StaticDatePicker/StaticDatePicker.tsx b/packages/material-ui-lab/src/StaticDatePicker/StaticDatePicker.tsx new file mode 100644 index 00000000000000..aaac410cbd092e --- /dev/null +++ b/packages/material-ui-lab/src/StaticDatePicker/StaticDatePicker.tsx @@ -0,0 +1,213 @@ +import PropTypes from 'prop-types'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; +import { + BaseDatePickerProps, + datePickerConfig, + DatePickerGenericComponent, +} from '../DatePicker/DatePicker'; +import { StaticWrapper } from '../internal/pickers/wrappers/Wrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const StaticDatePicker = makePickerWithStateAndWrapper>( + StaticWrapper, + { + name: 'MuiStaticDatePicker', + ...datePickerConfig, + }, +) as DatePickerGenericComponent; + +(StaticDatePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Force static wrapper inner components to be rendered in mobile or desktop mode + * @default "static" + */ + displayStaticWrapperAs: PropTypes.oneOf(['desktop', 'mobile']), + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), +}; + +export type StaticDatePickerProps = React.ComponentProps; + +export default StaticDatePicker; diff --git a/packages/material-ui-lab/src/StaticDatePicker/index.ts b/packages/material-ui-lab/src/StaticDatePicker/index.ts new file mode 100644 index 00000000000000..be42d67e460d2d --- /dev/null +++ b/packages/material-ui-lab/src/StaticDatePicker/index.ts @@ -0,0 +1,2 @@ +export * from './StaticDatePicker'; +export { default } from './StaticDatePicker'; diff --git a/packages/material-ui-lab/src/StaticDateRangePicker/StaticDateRangePicker.tsx b/packages/material-ui-lab/src/StaticDateRangePicker/StaticDateRangePicker.tsx new file mode 100644 index 00000000000000..5559b1729626c1 --- /dev/null +++ b/packages/material-ui-lab/src/StaticDateRangePicker/StaticDateRangePicker.tsx @@ -0,0 +1,329 @@ +import PropTypes from 'prop-types'; +import { makeDateRangePicker } from '../DateRangePicker/makeDateRangePicker'; +import StaticWrapper from '../internal/pickers/wrappers/StaticWrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const StaticDateRangePicker = makeDateRangePicker('MuiPickersDateRangePicker', StaticWrapper); + +(StaticDateRangePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * If `true`, `onChange` is fired on click even if the same date is selected. + * @default false + */ + allowSameDateSelection: PropTypes.bool, + /** + * The number of calendars that render on **desktop**. + * @default 2 + */ + calendars: PropTypes.oneOf([1, 2, 3]), + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * if `true` after selecting `start` date calendar will not automatically switch to the month of `end` date + * @default false + */ + disableAutoMonthSwitching: PropTypes.bool, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * If `true`, todays date is rendering without highlighting with circle. + * @default false + */ + disableHighlightToday: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Force static wrapper inner components to be rendered in mobile or desktop mode + * @default "static" + */ + displayStaticWrapperAs: PropTypes.oneOf(['desktop', 'mobile']), + /** + * Text for end input label and toolbar placeholder. + * @default "end" + */ + endText: PropTypes.node, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * Get aria-label text for switching between views button. + */ + getViewSwitchingButtonText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * Max selectable date. @DateIOType + * @default Date(2099-31-12) + */ + maxDate: PropTypes.any, + /** + * Min selectable date. @DateIOType + * @default Date(1900-01-01) + */ + minDate: PropTypes.any, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Custom renderer for `` days. @DateIOType + * @example (date, DateRangeDayProps) => + */ + renderDay: PropTypes.func, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `startProps` and `endProps` arguments of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api), + * that you need to forward to the range start/end inputs respectively. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example + * ```jsx + * ( + * + * + * to + * + * ; + * )} + * /> + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. + * @default false + */ + showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Text for start input label and toolbar placeholder. + * @default "Start" + */ + startText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + ).isRequired, +}; + +export type StaticDateRangePickerProps = React.ComponentProps; + +export type DateRange = import('../DateRangePicker/RangeTypes').DateRange; + +export default StaticDateRangePicker; diff --git a/packages/material-ui-lab/src/StaticDateRangePicker/index.ts b/packages/material-ui-lab/src/StaticDateRangePicker/index.ts new file mode 100644 index 00000000000000..2fdb62bb7b73f1 --- /dev/null +++ b/packages/material-ui-lab/src/StaticDateRangePicker/index.ts @@ -0,0 +1,2 @@ +export * from './StaticDateRangePicker'; +export { default } from './StaticDateRangePicker'; diff --git a/packages/material-ui-lab/src/StaticDateTimePicker/StaticDateTimePicker.tsx b/packages/material-ui-lab/src/StaticDateTimePicker/StaticDateTimePicker.tsx new file mode 100644 index 00000000000000..d5cecf135339e2 --- /dev/null +++ b/packages/material-ui-lab/src/StaticDateTimePicker/StaticDateTimePicker.tsx @@ -0,0 +1,405 @@ +import PropTypes from 'prop-types'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; +import { + BaseDateTimePickerProps, + dateTimePickerConfig, + DateTimePickerGenericComponent, +} from '../DateTimePicker/DateTimePicker'; +import { StaticWrapper } from '../internal/pickers/wrappers/Wrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const StaticDateTimePicker = makePickerWithStateAndWrapper>( + StaticWrapper, + { + name: 'MuiStaticDateTimePicker', + ...dateTimePickerConfig, + }, +) as DateTimePickerGenericComponent; + +(StaticDateTimePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * If `true`, `onChange` is fired on click even if the same date is selected. + * @default false + */ + allowSameDateSelection: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ + ampm: PropTypes.bool, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * Date tab icon. + */ + dateRangeIcon: PropTypes.node, + /** + * Default calendar month displayed when `value={null}`. + * @default `new Date()` + */ + defaultCalendarMonth: PropTypes.any, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Disable future dates. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * If `true`, todays date is rendering without highlighting with circle. + * @default false + */ + disableHighlightToday: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Disable past dates. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Force static wrapper inner components to be rendered in mobile or desktop mode + * @default "static" + */ + displayStaticWrapperAs: PropTypes.oneOf(['desktop', 'mobile']), + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * Get aria-label text for switching between views button. + */ + getViewSwitchingButtonText: PropTypes.func, + /** + * To show tabs. + */ + hideTabs: PropTypes.bool, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Props to pass to left arrow button. + */ + leftArrowButtonProps: PropTypes.object, + /** + * Left arrow icon aria-label text. + */ + leftArrowButtonText: PropTypes.string, + /** + * Left arrow icon. + */ + leftArrowIcon: PropTypes.node, + /** + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false + */ + loading: PropTypes.bool, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Minimal selectable moment of time with binding to date, to set max time in each day use `maxTime`. + */ + maxDateTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + maxTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minDate: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Minimal selectable moment of time with binding to date, to set min time in each day use `minTime`. + */ + minDateTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback firing on month change. @DateIOType + */ + onMonthChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Callback fired on view change. + */ + onViewChange: PropTypes.func, + /** + * Callback firing on year change @DateIOType. + */ + onYearChange: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * First view to show. + */ + openTo: PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'seconds', 'year']), + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * Disable heavy animations. + * @default /(android)/i.test(window.navigator.userAgent). + */ + reduceAnimations: PropTypes.bool, + /** + * Custom renderer for day. Check [DayComponentProps api](https://material-ui-pickers.dev/api/Day) @DateIOType. + */ + renderDay: PropTypes.func, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading: PropTypes.func, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Props to pass to right arrow button. + */ + rightArrowButtonProps: PropTypes.object, + /** + * Right arrow icon aria-label text. + */ + rightArrowButtonText: PropTypes.string, + /** + * Right arrow icon. + */ + rightArrowIcon: PropTypes.node, + /** + * Disable specific date. @DateIOType + */ + shouldDisableDate: PropTypes.func, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, + /** + * If `true`, days that have `outsideCurrentMonth={true}` are displayed. + * @default false + */ + showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Time tab icon. + */ + timeIcon: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Array of views to show. + */ + views: PropTypes.arrayOf( + PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'year']).isRequired, + ), +}; + +export type StaticDateTimePickerProps = React.ComponentProps; + +export default StaticDateTimePicker; diff --git a/packages/material-ui-lab/src/StaticDateTimePicker/index.ts b/packages/material-ui-lab/src/StaticDateTimePicker/index.ts new file mode 100644 index 00000000000000..50a219b185cb90 --- /dev/null +++ b/packages/material-ui-lab/src/StaticDateTimePicker/index.ts @@ -0,0 +1,2 @@ +export * from './StaticDateTimePicker'; +export { default } from './StaticDateTimePicker'; diff --git a/packages/material-ui-lab/src/StaticTimePicker/StaticTimePicker.tsx b/packages/material-ui-lab/src/StaticTimePicker/StaticTimePicker.tsx new file mode 100644 index 00000000000000..83d1449f6232f1 --- /dev/null +++ b/packages/material-ui-lab/src/StaticTimePicker/StaticTimePicker.tsx @@ -0,0 +1,253 @@ +import PropTypes from 'prop-types'; +import { makePickerWithStateAndWrapper } from '../internal/pickers/Picker/makePickerWithState'; +import { + BaseTimePickerProps, + timePickerConfig, + TimePickerGenericComponent, +} from '../TimePicker/TimePicker'; +import { StaticWrapper } from '../internal/pickers/wrappers/Wrapper'; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const StaticTimePicker = makePickerWithStateAndWrapper(StaticWrapper, { + name: 'MuiStaticTimePicker', + ...timePickerConfig, +}) as TimePickerGenericComponent; + +(StaticTimePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ + ampm: PropTypes.bool, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Force static wrapper inner components to be rendered in mobile or desktop mode + * @default "static" + */ + displayStaticWrapperAs: PropTypes.oneOf(['desktop', 'mobile']), + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * First view to show. + */ + openTo: PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'seconds', 'year']), + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Array of views to show. + */ + views: PropTypes.arrayOf(PropTypes.oneOf(['hours', 'minutes', 'seconds']).isRequired), +}; + +export type StaticTimePickerProps = React.ComponentProps; + +export default StaticTimePicker; diff --git a/packages/material-ui-lab/src/StaticTimePicker/index.ts b/packages/material-ui-lab/src/StaticTimePicker/index.ts new file mode 100644 index 00000000000000..708e01be9ccbb7 --- /dev/null +++ b/packages/material-ui-lab/src/StaticTimePicker/index.ts @@ -0,0 +1,2 @@ +export * from './StaticTimePicker'; +export { default } from './StaticTimePicker'; diff --git a/packages/material-ui-lab/src/TimePicker/TimePicker.spec.tsx b/packages/material-ui-lab/src/TimePicker/TimePicker.spec.tsx new file mode 100644 index 00000000000000..f37cd553bc4cdd --- /dev/null +++ b/packages/material-ui-lab/src/TimePicker/TimePicker.spec.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import moment from 'moment'; +import { TimePicker, ClockPicker } from '@material-ui/lab'; + + date?.set({ second: 0 })} + renderInput={() => } +/>; + +// Allows inferring for side props + date?.set({ second: 0 })} + renderInput={() => } +/>; + +// External components are generic as well + view="hours" date={null} onChange={(date) => date?.getDate()} />; diff --git a/packages/material-ui-lab/src/TimePicker/TimePicker.test.tsx b/packages/material-ui-lab/src/TimePicker/TimePicker.test.tsx new file mode 100644 index 00000000000000..375fefb1730051 --- /dev/null +++ b/packages/material-ui-lab/src/TimePicker/TimePicker.test.tsx @@ -0,0 +1,287 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import { spy } from 'sinon'; +import { expect } from 'chai'; +import { fireEvent, fireTouchChangedEvent, screen } from 'test/utils'; +import { TimePickerProps } from '@material-ui/lab/TimePicker'; +import MobileTimePicker from '@material-ui/lab/MobileTimePicker'; +import DesktopTimePicker from '@material-ui/lab/DesktopTimePicker'; +import { createPickerRender, adapterToUse, getByMuiTest } from '../internal/pickers/test-utils'; + +describe('', () => { + const render = createPickerRender({ strict: false }); + + function createMouseEventWithOffsets( + type: 'mousedown' | 'mousemove' | 'mouseup', + { offsetX, offsetY, ...eventOptions }: { offsetX: number; offsetY: number } & MouseEventInit, + ) { + const event = new window.MouseEvent(type, { + bubbles: true, + cancelable: true, + ...eventOptions, + }); + + Object.defineProperty(event, 'offsetX', { get: () => offsetX }); + Object.defineProperty(event, 'offsetY', { get: () => offsetY }); + + return event; + } + + it('accepts time on clock mouse move', () => { + const onChangeMock = spy(); + render( + } + />, + ); + + const fakeEventOptions = { + buttons: 1, + offsetX: 20, + offsetY: 15, + }; + + fireEvent(getByMuiTest('clock'), createMouseEventWithOffsets('mousemove', fakeEventOptions)); + fireEvent(getByMuiTest('clock'), createMouseEventWithOffsets('mouseup', fakeEventOptions)); + + expect(getByMuiTest('hours')).to.have.text('11'); + expect(onChangeMock.callCount).to.equal(1); + }); + + it('accepts time on clock touch move', function test() { + if (typeof window.Touch === 'undefined' || typeof window.TouchEvent === 'undefined') { + this.skip(); + } + + const onChangeMock = spy(); + render( + } + />, + ); + + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', { + changedTouches: [{ clientX: 20, clientY: 15 }], + }); + expect(getByMuiTest('minutes')).to.have.text('53'); + }); + + it('allows to navigate between timepicker views using arrow switcher', () => { + render( + {}} + renderInput={(params) => } + />, + ); + + const prevViewButton = screen.getByLabelText('open previous view'); + const nextViewButton = screen.getByLabelText('open next view'); + + expect(screen.getByLabelText(/Select Hours/i)).toBeVisible(); + expect(prevViewButton).to.have.attribute('disabled'); + + fireEvent.click(nextViewButton); + expect(screen.getByLabelText(/Select minutes/)).toBeVisible(); + + expect(prevViewButton).not.to.have.attribute('disabled'); + expect(nextViewButton).not.to.have.attribute('disabled'); + + fireEvent.click(nextViewButton); + expect(screen.getByLabelText(/Select seconds/)).toBeVisible(); + expect(nextViewButton).to.have.attribute('disabled'); + }); + + it('allows to select full date from empty', function test() { + if (typeof window.Touch === 'undefined' || typeof window.TouchEvent === 'undefined') { + this.skip(); + } + + function TimePickerWithState() { + const [time, setTime] = React.useState(null); + + return ( + setTime(newTime)} + renderInput={(params) => } + /> + ); + } + + render(); + + expect(getByMuiTest('hours')).to.have.text('--'); + expect(getByMuiTest('minutes')).to.have.text('--'); + + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', { + changedTouches: [ + { + clientX: 20, + clientY: 15, + }, + ], + }); + + expect(getByMuiTest('hours')).not.to.have.text('--'); + expect(getByMuiTest('minutes')).not.to.have.text('--'); + }); + + context('Time validation on touch ', () => { + before(function beforeHook() { + if (typeof window.Touch === 'undefined' || typeof window.TouchEvent === 'undefined') { + this.skip(); + } + }); + + const clockMouseEvent = { + '13:--': { + changedTouches: [ + { + clientX: 166, + clientY: 76, + }, + ], + }, + '20:--': { + changedTouches: [ + { + clientX: 66, + clientY: 157, + }, + ], + }, + '--:10': { + changedTouches: [ + { + clientX: 220, + clientY: 72, + }, + ], + }, + '--:20': { + changedTouches: [ + { + clientX: 222, + clientY: 180, + }, + ], + }, + }; + + beforeEach(() => { + render( + } + open + ampm={false} + onChange={() => {}} + views={['hours', 'minutes', 'seconds']} + value={adapterToUse.date('2018-01-01T00:00:00.000')} + minTime={new Date(0, 0, 0, 12, 15, 15)} + maxTime={new Date(0, 0, 0, 15, 45, 30)} + />, + ); + }); + + it('should select enabled hour', () => { + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['13:--']); + expect(getByMuiTest('hours')).to.have.text('13'); + }); + + it('should select enabled minute', () => { + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['13:--']); + fireTouchChangedEvent(getByMuiTest('clock'), 'touchend', clockMouseEvent['13:--']); + + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['--:20']); + + expect(getByMuiTest('minutes')).to.have.text('20'); + }); + + it('should not select minute when hour is disabled ', () => { + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['20:--']); + fireTouchChangedEvent(getByMuiTest('clock'), 'touchend', clockMouseEvent['20:--']); + + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['--:20']); + }); + + it('should not select disabled hour', () => { + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['20:--']); + expect(getByMuiTest('hours')).to.have.text('00'); + }); + + it('should not select disabled second', () => { + fireEvent.click(getByMuiTest('seconds')); + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['--:20']); + + expect(getByMuiTest('seconds')).to.have.text('00'); + }); + + it('should select enabled second', () => { + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['13:--']); + fireTouchChangedEvent(getByMuiTest('clock'), 'touchend', clockMouseEvent['13:--']); + + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['--:20']); + fireTouchChangedEvent(getByMuiTest('clock'), 'touchend', clockMouseEvent['--:20']); + + fireTouchChangedEvent(getByMuiTest('clock'), 'touchmove', clockMouseEvent['--:10']); + + expect(getByMuiTest('seconds')).to.have.text('10'); + }); + }); + + context('input validation', () => { + const createTime = (time: string) => new Date(`01/01/2000 ${time}`); + const shouldDisableTime: TimePickerProps['shouldDisableTime'] = (value) => value === 10; + + [ + { expectedError: 'invalidDate', props: {}, input: 'invalidText' }, + { expectedError: 'minTime', props: { minTime: createTime('08:00') }, input: '03:00' }, + { expectedError: 'maxTime', props: { maxTime: createTime('08:00') }, input: '12:00' }, + { expectedError: 'shouldDisableTime-hours', props: { shouldDisableTime }, input: '10:00' }, + { expectedError: 'shouldDisableTime-minutes', props: { shouldDisableTime }, input: '00:10' }, + ].forEach(({ props, input, expectedError }) => { + it(`should dispatch "${expectedError}" error`, () => { + const onErrorMock = spy(); + + // we are running validation on value change + function TimePickerInput() { + const [time, setTime] = React.useState(null); + + return ( + setTime(newTime)} + renderInput={(inputProps) => } + {...props} + /> + ); + } + + render(); + + fireEvent.change(screen.getByRole('textbox'), { + target: { + value: input, + }, + }); + + expect(onErrorMock.calledWith(expectedError)).to.be.equal(true); + }); + }); + }); +}); diff --git a/packages/material-ui-lab/src/TimePicker/TimePicker.tsx b/packages/material-ui-lab/src/TimePicker/TimePicker.tsx new file mode 100644 index 00000000000000..e1cb5457b17cd1 --- /dev/null +++ b/packages/material-ui-lab/src/TimePicker/TimePicker.tsx @@ -0,0 +1,367 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ClockIcon from '../internal/svg-icons/Clock'; +import { ParsableDate } from '../internal/pickers/constants/prop-types'; +import TimePickerToolbar from './TimePickerToolbar'; +import { ExportedClockPickerProps } from '../ClockPicker/ClockPicker'; +import { ResponsiveWrapper } from '../internal/pickers/wrappers/ResponsiveWrapper'; +import { pick12hOr24hFormat } from '../internal/pickers/text-field-helper'; +import { useUtils, MuiPickersAdapter } from '../internal/pickers/hooks/useUtils'; +import { validateTime, TimeValidationError } from '../internal/pickers/time-utils'; +import { WithViewsProps, AllSharedPickerProps } from '../internal/pickers/Picker/SharedPickerProps'; +import { ValidationProps, makeValidationHook } from '../internal/pickers/hooks/useValidation'; +import { + useParsedDate, + OverrideParsableDateProps, +} from '../internal/pickers/hooks/date-helpers-hooks'; +import { SomeWrapper } from '../internal/pickers/wrappers/Wrapper'; +import { + SharedPickerProps, + makePickerWithStateAndWrapper, +} from '../internal/pickers/Picker/makePickerWithState'; + +export interface BaseTimePickerProps + extends ValidationProps>, + WithViewsProps<'hours' | 'minutes' | 'seconds'>, + OverrideParsableDateProps, 'minTime' | 'maxTime'> {} + +export function getTextFieldAriaText(value: ParsableDate, utils: MuiPickersAdapter) { + return value && utils.isValid(utils.date(value)) + ? `Choose time, selected time is ${utils.format(utils.date(value), 'fullTime')}` + : 'Choose time'; +} + +function useInterceptProps({ + ampm, + inputFormat, + maxTime: __maxTime, + minTime: __minTime, + openTo = 'hours', + views = ['hours', 'minutes'], + ...other +}: BaseTimePickerProps & AllSharedPickerProps) { + const utils = useUtils(); + + const minTime = useParsedDate(__minTime); + const maxTime = useParsedDate(__maxTime); + const willUseAmPm = ampm ?? utils.is12HourCycleInCurrentLocale(); + + return { + views, + openTo, + minTime, + maxTime, + ampm: willUseAmPm, + acceptRegex: willUseAmPm ? /[\dapAP]/gi : /\d/gi, + mask: '__:__', + disableMaskedInput: willUseAmPm, + getOpenDialogAriaText: getTextFieldAriaText, + openPickerIcon: , + inputFormat: pick12hOr24hFormat(inputFormat, willUseAmPm, { + localized: utils.formats.fullTime, + '12h': utils.formats.fullTime12h, + '24h': utils.formats.fullTime24h, + }), + ...other, + }; +} + +export const timePickerConfig = { + useInterceptProps, + useValidation: makeValidationHook( + validateTime, + ), + DefaultToolbarComponent: TimePickerToolbar, +}; + +export type TimePickerGenericComponent = ( + props: BaseTimePickerProps & SharedPickerProps, +) => JSX.Element; + +/** + * @ignore - do not document. + */ +/* @GeneratePropTypes */ +const TimePicker = makePickerWithStateAndWrapper(ResponsiveWrapper, { + name: 'MuiTimePicker', + ...timePickerConfig, +}) as TimePickerGenericComponent; + +(TimePicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * Regular expression to detect "accepted" symbols. + * @default /\dap/gi + */ + acceptRegex: PropTypes.instanceOf(RegExp), + /** + * Enables keyboard listener for moving between days in calendar. + * @default currentWrapper !== 'static' + */ + allowKeyboardControl: PropTypes.bool, + /** + * 12h/24h view for hour selection clock. + * @default true + */ + ampm: PropTypes.bool, + /** + * Display ampm controls under the clock (instead of in the toolbar). + * @default false + */ + ampmInClock: PropTypes.bool, + /** + * "CANCEL" Text message + * @default "CANCEL" + */ + cancelText: PropTypes.node, + /** + * className applied to the root component. + */ + className: PropTypes.string, + /** + * If `true`, it shows the clear action in the picker dialog. + * @default false + */ + clearable: PropTypes.bool, + /** + * "CLEAR" Text message + * @default "CLEAR" + */ + clearText: PropTypes.node, + /** + * Allows to pass configured date-io adapter directly. More info [here](https://next.material-ui-pickers.dev/guides/date-adapter-passing) + * ```jsx + * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} + * ``` + */ + dateAdapter: PropTypes.object, + /** + * CSS media query when `Mobile` mode will be changed to `Desktop`. + * @default "@media (pointer: fine)" + * @example "@media (min-width: 720px)" or theme.breakpoints.up("sm") + */ + desktopModeMediaQuery: PropTypes.string, + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps: PropTypes.object, + /** + * If `true` the popup or dialog will immediately close after submitting full date. + * @default `true` for Desktop, `false` for Mobile (based on the chosen wrapper and `desktopModeMediaQuery` prop). + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the picker and text field are disabled. + */ + disabled: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format. + * @default false + */ + disableMaskedInput: PropTypes.bool, + /** + * Do not render open picker button (renders only text field with validation). + * @default false + */ + disableOpenPicker: PropTypes.bool, + /** + * Accessible text that helps user to understand which time and view is selected. + * @default (view, time) => `Select ${view}. Selected time is ${format(time, 'fullTime')}` + */ + getClockLabelText: PropTypes.func, + /** + * Get aria-label text for control that opens picker dialog. Aria-label text must include selected date. @DateIOType + * @default (value, utils) => `Choose date, selected date is ${utils.format(utils.date(value), 'fullDate')}` + */ + getOpenDialogAriaText: PropTypes.func, + /** + * @ignore + */ + ignoreInvalidInputs: PropTypes.bool, + /** + * Props to pass to keyboard input adornment. + */ + InputAdornmentProps: PropTypes.object, + /** + * Format string. + */ + inputFormat: PropTypes.string, + /** + * @ignore + */ + InputProps: PropTypes.object, + /** + * @ignore + */ + key: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** + * @ignore + */ + label: PropTypes.node, + /** + * Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__ or __/__/____ __:__ _M) + */ + mask: PropTypes.string, + /** + * @ignore + */ + maxTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * @ignore + */ + minTime: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * "OK" button text. + * @default "OK" + */ + okText: PropTypes.node, + /** + * Callback fired when date is accepted @DateIOType. + */ + onAccept: PropTypes.func, + /** + * Callback fired when the value (the selected date) changes. @DateIOType. + */ + onChange: PropTypes.func.isRequired, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + */ + onClose: PropTypes.func, + /** + * Callback that fired when input value or new `value` prop validation returns **new** validation error (or value is valid after error). + * In case of validation error detected `reason` prop return non-null value and `TextField` must be displayed in `error` state. + * This can be used to render appropriate form error. + * + * [Read the guide](https://next.material-ui-pickers.dev/guides/forms) about form integration and error displaying. + * @DateIOType + */ + onError: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + */ + onOpen: PropTypes.func, + /** + * Control the popup or dialog open state. + */ + open: PropTypes.bool, + /** + * Props to pass to keyboard adornment button. + */ + OpenPickerButtonProps: PropTypes.object, + /** + * Icon displaying for open picker button. + */ + openPickerIcon: PropTypes.node, + /** + * First view to show. + */ + openTo: PropTypes.oneOf(['date', 'hours', 'minutes', 'month', 'seconds', 'year']), + /** + * Force rendering in particular orientation. + */ + orientation: PropTypes.oneOf(['landscape', 'portrait']), + /** + * Popper props passed down to [Popper](https://material-ui.com/api/popper/) component. + */ + PopperProps: PropTypes.object, + /** + * Make picker read only. + */ + readOnly: PropTypes.bool, + /** + * The `renderInput` prop allows you to customize the rendered input. + * The `props` argument of this render prop contains props of [TextField](https://material-ui.com/api/text-field/#textfield-api) that you need to forward. + * Pay specific attention to the `ref` and `inputProps` keys. + * @example ```jsx + * renderInput={props => } + * ```` + */ + renderInput: PropTypes.func.isRequired, + /** + * Custom formatter to be passed into Rifm component. + */ + rifmFormatter: PropTypes.func, + /** + * Dynamically check if time is disabled or not. + * If returns `false` appropriate time point will ot be acceptable. + */ + shouldDisableTime: PropTypes.func, + /** + * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. + * @default false + */ + showTodayButton: PropTypes.bool, + /** + * If `true`, show the toolbar even in desktop mode. + */ + showToolbar: PropTypes.bool, + /** + * "TODAY" Text message + * @default "TODAY" + */ + todayText: PropTypes.node, + /** + * Component that will replace default toolbar renderer. + */ + ToolbarComponent: PropTypes.elementType, + /** + * Date format, that is displaying in toolbar. + */ + toolbarFormat: PropTypes.string, + /** + * Mobile picker date value placeholder, displaying if `value` === `null`. + * @default "–" + */ + toolbarPlaceholder: PropTypes.node, + /** + * Mobile picker title, displaying in the toolbar. + * @default "SELECT DATE" + */ + toolbarTitle: PropTypes.node, + /** + * Custom component for popper [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). + */ + TransitionComponent: PropTypes.elementType, + /** + * The value of the picker. + */ + value: PropTypes.oneOfType([ + PropTypes.any, + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), + /** + * Array of views to show. + */ + views: PropTypes.arrayOf(PropTypes.oneOf(['hours', 'minutes', 'seconds']).isRequired), +}; + +export type TimePickerProps = React.ComponentProps; + +export default TimePicker; diff --git a/packages/material-ui-lab/src/TimePicker/TimePickerToolbar.tsx b/packages/material-ui-lab/src/TimePicker/TimePickerToolbar.tsx new file mode 100644 index 00000000000000..72d98620e8fd1a --- /dev/null +++ b/packages/material-ui-lab/src/TimePicker/TimePickerToolbar.tsx @@ -0,0 +1,168 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { useTheme, createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; +import ToolbarText from '../internal/pickers/PickersToolbarText'; +import ToolbarButton from '../internal/pickers/PickersToolbarButton'; +import PickerToolbar from '../internal/pickers/PickersToolbar'; +import { arrayIncludes } from '../internal/pickers/utils'; +import { useUtils } from '../internal/pickers/hooks/useUtils'; +import { useMeridiemMode } from '../internal/pickers/hooks/date-helpers-hooks'; +import { ToolbarComponentProps } from '../internal/pickers/typings/BasePicker'; + +export const styles = createStyles({ + separator: { + outline: 0, + margin: '0 4px 0 2px', + cursor: 'default', + }, + hourMinuteLabel: { + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'flex-end', + }, + hourMinuteLabelLandscape: { + marginTop: 'auto', + }, + hourMinuteLabelReverse: { + flexDirection: 'row-reverse', + }, + ampmSelection: { + display: 'flex', + flexDirection: 'column', + marginRight: 'auto', + marginLeft: 12, + }, + ampmLandscape: { + margin: '4px 0 auto', + flexDirection: 'row', + justifyContent: 'space-around', + flexBasis: '100%', + }, + ampmLabel: { + fontSize: 17, + }, + penIconLandscape: { + marginTop: 'auto', + }, +}); + +export type TimePickerToolbarClassKey = keyof WithStyles['classes']; + +const clockTypographyVariant = 'h3'; + +/** + * @ignore - internal component. + */ +const TimePickerToolbar: React.FC> = (props) => { + const { + ampm, + ampmInClock, + classes, + date, + isLandscape, + isMobileKeyboardViewOpen, + onChange, + openView, + setOpenView, + toggleMobileKeyboardView, + toolbarTitle = 'SELECT TIME', + views, + ...other + } = props; + const utils = useUtils(); + const theme = useTheme(); + const showAmPmControl = Boolean(ampm && !ampmInClock); + const { meridiemMode, handleMeridiemChange } = useMeridiemMode(date, ampm, onChange); + + const formatHours = (time: unknown) => + ampm ? utils.format(time, 'hours12h') : utils.format(time, 'hours24h'); + + const separator = ( + + ); + + return ( + +
+ {arrayIncludes(views, 'hours') && ( + setOpenView('hours')} + selected={openView === 'hours'} + value={date ? formatHours(date) : '--'} + /> + )} + {arrayIncludes(views, ['hours', 'minutes']) && separator} + {arrayIncludes(views, 'minutes') && ( + setOpenView('minutes')} + selected={openView === 'minutes'} + value={date ? utils.format(date, 'minutes') : '--'} + /> + )} + {arrayIncludes(views, ['minutes', 'seconds']) && separator} + {arrayIncludes(views, 'seconds') && ( + setOpenView('seconds')} + selected={openView === 'seconds'} + value={date ? utils.format(date, 'seconds') : '--'} + /> + )} +
+ {showAmPmControl && ( +
+ handleMeridiemChange('am')} + /> + handleMeridiemChange('pm')} + /> +
+ )} +
+ ); +}; + +export default withStyles(styles, { name: 'MuiTimePickerToolbar' })(TimePickerToolbar); diff --git a/packages/material-ui-lab/src/TimePicker/index.tsx b/packages/material-ui-lab/src/TimePicker/index.tsx new file mode 100644 index 00000000000000..7b71a6a9b5ccbf --- /dev/null +++ b/packages/material-ui-lab/src/TimePicker/index.tsx @@ -0,0 +1,2 @@ +export * from './TimePicker'; +export { default } from './TimePicker'; diff --git a/packages/material-ui-lab/src/YearPicker/PickersYear.tsx b/packages/material-ui-lab/src/YearPicker/PickersYear.tsx new file mode 100644 index 00000000000000..e89b844c896640 --- /dev/null +++ b/packages/material-ui-lab/src/YearPicker/PickersYear.tsx @@ -0,0 +1,115 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { useForkRef } from '@material-ui/core/utils'; +import { createStyles, WithStyles, withStyles, Theme, alpha } from '@material-ui/core/styles'; +import { onSpaceOrEnter } from '../internal/pickers/utils'; +import { useCanAutoFocus } from '../internal/pickers/hooks/useCanAutoFocus'; +import { WrapperVariantContext } from '../internal/pickers/wrappers/WrapperVariantContext'; + +export interface YearProps { + children: React.ReactNode; + disabled?: boolean; + onSelect: (value: number) => void; + selected: boolean; + focused: boolean; + value: number; + allowKeyboardControl?: boolean; + forwardedRef?: React.Ref; +} + +export const styles = (theme: Theme) => + createStyles({ + root: { + flexBasis: '33.3%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + modeDesktop: { + flexBasis: '25%', + }, + yearButton: { + color: 'unset', + backgroundColor: 'transparent', + border: 'none', + outline: 0, + ...theme.typography.subtitle1, + margin: '8px 0', + height: 36, + width: 72, + borderRadius: 16, + cursor: 'pointer', + '&:focus, &:hover': { + backgroundColor: alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), + }, + '&$disabled': { + color: theme.palette.text.secondary, + }, + '&$selected': { + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, + '&:focus, &:hover': { + backgroundColor: theme.palette.primary.dark, + }, + }, + }, + disabled: {}, + selected: {}, + }); + +export type PickersYearClassKey = keyof WithStyles['classes']; + +/** + * @ignore - internal component. + */ +const PickersYear = React.forwardRef>( + (props, forwardedRef) => { + const { + allowKeyboardControl, + classes, + children, + disabled, + focused, + onSelect, + selected, + value, + } = props; + const ref = React.useRef(null); + const refHandle = useForkRef(ref, forwardedRef as React.Ref); + const canAutoFocus = useCanAutoFocus(); + const wrapperVariant = React.useContext(WrapperVariantContext); + + React.useEffect(() => { + if (canAutoFocus && focused && ref.current && !disabled && allowKeyboardControl) { + ref.current.focus(); + } + }, [allowKeyboardControl, canAutoFocus, disabled, focused]); + + return ( +
+ +
+ ); + }, +); + +export default withStyles(styles, { name: 'MuiPickersYear' })(PickersYear); diff --git a/packages/material-ui-lab/src/YearPicker/YearPicker.test.tsx b/packages/material-ui-lab/src/YearPicker/YearPicker.test.tsx new file mode 100644 index 00000000000000..2028aa1eb76328 --- /dev/null +++ b/packages/material-ui-lab/src/YearPicker/YearPicker.test.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { spy } from 'sinon'; +import { expect } from 'chai'; +import { getClasses, createMount, fireEvent, screen, describeConformance } from 'test/utils'; +import LocalizationProvider from '@material-ui/lab/LocalizationProvider'; +import DateFnsAdapter from '@material-ui/lab/dateAdapter/date-fns'; +import YearPicker from '@material-ui/lab/YearPicker'; +import { createPickerRender } from '../internal/pickers/test-utils'; + +describe('', () => { + const mount = createMount(); + const render = createPickerRender({ strict: false }); + let classes: Record; + + const localizedMount = (node: React.ReactNode) => { + return mount({node}); + }; + + before(() => { + classes = getClasses( + false} + date={new Date()} + onChange={() => {}} + />, + ); + }); + + describeConformance( + false} + date={new Date()} + onChange={() => {}} + />, + () => ({ + classes, + inheritComponent: 'div', + mount: localizedMount, + refInstanceof: window.HTMLDivElement, + // cannot test reactTestRenderer because of required context + skip: ['componentProp', 'propsSpread', 'reactTestRenderer'], + }), + ); + + it('allows to pick year standalone', () => { + const onChangeMock = spy(); + render( + false} + date={new Date('2019-02-02T00:00:00.000')} + onChange={onChangeMock} + />, + ); + + fireEvent.click(screen.getByText('2025', { selector: 'button' })); + expect(onChangeMock.calledWith(new Date('2025-02-02T00:00:00.000'))).to.equal(true); + }); +}); diff --git a/packages/material-ui-lab/src/YearPicker/YearPicker.tsx b/packages/material-ui-lab/src/YearPicker/YearPicker.tsx new file mode 100644 index 00000000000000..7c84cb70e21b01 --- /dev/null +++ b/packages/material-ui-lab/src/YearPicker/YearPicker.tsx @@ -0,0 +1,239 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { createStyles, WithStyles, withStyles, useTheme } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import PickersYear from './PickersYear'; +import { useUtils, useNow } from '../internal/pickers/hooks/useUtils'; +import { PickerOnChangeFn } from '../internal/pickers/hooks/useViews'; +import { findClosestEnabledDate } from '../internal/pickers/date-utils'; +import { PickerSelectionState } from '../internal/pickers/hooks/usePickerState'; +import { WrapperVariantContext } from '../internal/pickers/wrappers/WrapperVariantContext'; +import { useGlobalKeyDown, keycode as keys } from '../internal/pickers/hooks/useKeyDown'; + +export interface ExportedYearPickerProps { + /** + * Callback firing on year change @DateIOType. + */ + onYearChange?: (date: TDate) => void; + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear?: (day: TDate) => boolean; +} + +export interface YearPickerProps extends ExportedYearPickerProps { + allowKeyboardControl?: boolean; + onFocusedDayChange?: (day: TDate) => void; + date: TDate | null; + disableFuture?: boolean | null; + disablePast?: boolean | null; + isDateDisabled: (day: TDate) => boolean; + maxDate: TDate; + minDate: TDate; + onChange: PickerOnChangeFn; + className?: string; +} + +export const styles = createStyles({ + root: { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + overflowY: 'auto', + height: '100%', + margin: '0 4px', + }, +}); + +export type YearPickerClassKey = keyof WithStyles['classes']; + +/** + * @ignore - do not document. + */ +const YearPicker = React.forwardRef(function YearPicker( + props: YearPickerProps & WithStyles, + ref: React.Ref, +) { + const { + allowKeyboardControl, + classes, + className, + date, + disableFuture, + disablePast, + isDateDisabled, + maxDate, + minDate, + onChange, + onFocusedDayChange, + onYearChange, + shouldDisableYear, + } = props; + + const now = useNow(); + const theme = useTheme(); + const utils = useUtils(); + + const selectedDate = date || now; + const currentYear = utils.getYear(selectedDate); + const wrapperVariant = React.useContext(WrapperVariantContext); + const selectedYearRef = React.useRef(null); + const [focusedYear, setFocusedYear] = React.useState(currentYear); + + const handleYearSelection = React.useCallback( + (year: number, isFinish: PickerSelectionState = 'finish') => { + const submitDate = (newDate: TDate) => { + onChange(newDate, isFinish); + + if (onFocusedDayChange) { + onFocusedDayChange(newDate || now); + } + + if (onYearChange) { + onYearChange(newDate); + } + }; + + const newDate = utils.setYear(selectedDate, year); + if (isDateDisabled(newDate)) { + const closestEnabledDate = findClosestEnabledDate({ + utils, + date: newDate, + minDate, + maxDate, + disablePast: Boolean(disablePast), + disableFuture: Boolean(disableFuture), + shouldDisableDate: isDateDisabled, + }); + + submitDate(closestEnabledDate || now); + } else { + submitDate(newDate); + } + }, + [ + utils, + now, + selectedDate, + isDateDisabled, + onChange, + onFocusedDayChange, + onYearChange, + minDate, + maxDate, + disablePast, + disableFuture, + ], + ); + + const focusYear = React.useCallback( + (year: number) => { + if (!isDateDisabled(utils.setYear(selectedDate, year))) { + setFocusedYear(year); + } + }, + [selectedDate, isDateDisabled, utils], + ); + + const yearsInRow = wrapperVariant === 'desktop' ? 4 : 3; + const nowFocusedYear = focusedYear || currentYear; + useGlobalKeyDown(Boolean(allowKeyboardControl), { + [keys.ArrowUp]: () => focusYear(nowFocusedYear - yearsInRow), + [keys.ArrowDown]: () => focusYear(nowFocusedYear + yearsInRow), + [keys.ArrowLeft]: () => focusYear(nowFocusedYear + (theme.direction === 'ltr' ? -1 : 1)), + [keys.ArrowRight]: () => focusYear(nowFocusedYear + (theme.direction === 'ltr' ? 1 : -1)), + }); + + return ( +
+ {utils.getYearRange(minDate, maxDate).map((year) => { + const yearNumber = utils.getYear(year); + const selected = yearNumber === currentYear; + + return ( + + {utils.format(year, 'year')} + + ); + })} +
+ ); +}); + +(YearPicker as any).propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * @ignore + */ + allowKeyboardControl: PropTypes.bool, + /** + * @ignore + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, + /** + * @ignore + */ + date: PropTypes.any, + /** + * @ignore + */ + disableFuture: PropTypes.bool, + /** + * @ignore + */ + disablePast: PropTypes.bool, + /** + * @ignore + */ + isDateDisabled: PropTypes.func.isRequired, + /** + * @ignore + */ + maxDate: PropTypes.any.isRequired, + /** + * @ignore + */ + minDate: PropTypes.any.isRequired, + /** + * @ignore + */ + onChange: PropTypes.func.isRequired, + /** + * @ignore + */ + onFocusedDayChange: PropTypes.func, + /** + * Callback firing on year change @DateIOType. + */ + onYearChange: PropTypes.func, + /** + * Disable specific years dynamically. + * Works like `shouldDisableDate` but for year selection view. @DateIOType. + */ + shouldDisableYear: PropTypes.func, +}; + +export default withStyles(styles, { name: 'MuiPickersYearSelection' })(YearPicker) as ( + props: YearPickerProps & React.RefAttributes, +) => JSX.Element; diff --git a/packages/material-ui-lab/src/YearPicker/index.ts b/packages/material-ui-lab/src/YearPicker/index.ts new file mode 100644 index 00000000000000..1814639f1b5e13 --- /dev/null +++ b/packages/material-ui-lab/src/YearPicker/index.ts @@ -0,0 +1,4 @@ +export { default } from './YearPicker'; + +export type YearPickerClassKey = import('./YearPicker').YearPickerClassKey; +export type YearPickerProps = import('./YearPicker').YearPickerProps; diff --git a/packages/pickers/lib/adapter/date-fns.ts b/packages/material-ui-lab/src/dateAdapter/date-fns.ts similarity index 100% rename from packages/pickers/lib/adapter/date-fns.ts rename to packages/material-ui-lab/src/dateAdapter/date-fns.ts diff --git a/packages/pickers/lib/adapter/dayjs.ts b/packages/material-ui-lab/src/dateAdapter/dayjs.ts similarity index 100% rename from packages/pickers/lib/adapter/dayjs.ts rename to packages/material-ui-lab/src/dateAdapter/dayjs.ts diff --git a/packages/pickers/lib/adapter/luxon.ts b/packages/material-ui-lab/src/dateAdapter/luxon.ts similarity index 100% rename from packages/pickers/lib/adapter/luxon.ts rename to packages/material-ui-lab/src/dateAdapter/luxon.ts diff --git a/packages/pickers/lib/adapter/moment.ts b/packages/material-ui-lab/src/dateAdapter/moment.ts similarity index 100% rename from packages/pickers/lib/adapter/moment.ts rename to packages/material-ui-lab/src/dateAdapter/moment.ts diff --git a/packages/material-ui-lab/src/index.d.ts b/packages/material-ui-lab/src/index.d.ts index d0c63e8c4a56d6..1d3613aa221b68 100644 --- a/packages/material-ui-lab/src/index.d.ts +++ b/packages/material-ui-lab/src/index.d.ts @@ -10,12 +10,18 @@ export * from './AvatarGroup'; export { default as LoadingButton } from './LoadingButton'; export * from './LoadingButton'; +export { default as LocalizationProvider } from './LocalizationProvider'; +export * from './LocalizationProvider'; + export { default as Pagination } from './Pagination'; export * from './Pagination'; export { default as PaginationItem } from './PaginationItem'; export * from './PaginationItem'; +export { default as PickersDay } from './PickersDay'; +export * from './PickersDay'; + export { default as Rating } from './Rating'; export * from './Rating'; @@ -78,3 +84,66 @@ export * from './TreeView'; export { default as useAutocomplete } from './useAutocomplete'; export * from './useAutocomplete'; + +export { default as DayPicker } from './DayPicker'; +export * from './DayPicker'; + +export { default as DatePicker } from './DatePicker'; +export * from './DatePicker'; + +export { default as DesktopDatePicker } from './DesktopDatePicker'; +export * from './DesktopDatePicker'; + +export { default as MobileDatePicker } from './MobileDatePicker'; +export * from './MobileDatePicker'; + +export { default as StaticDatePicker } from './StaticDatePicker'; +export * from './StaticDatePicker'; + +export { default as TimePicker } from './TimePicker'; +export * from './TimePicker'; + +export { default as YearPicker } from './YearPicker'; +export * from './YearPicker'; + +export { default as DesktopTimePicker } from './DesktopTimePicker'; +export * from './DesktopTimePicker'; + +export { default as MobileTimePicker } from './MobileTimePicker'; +export * from './MobileTimePicker'; + +export { default as StaticTimePicker } from './StaticTimePicker'; +export * from './StaticTimePicker'; + +export { default as DateTimePicker } from './DateTimePicker'; +export * from './DateTimePicker'; + +export { default as DesktopDateTimePicker } from './DesktopDateTimePicker'; +export * from './DesktopDateTimePicker'; + +export { default as MobileDateTimePicker } from './MobileDateTimePicker'; +export * from './MobileDateTimePicker'; + +export { default as StaticDateTimePicker } from './StaticDateTimePicker'; +export * from './StaticDateTimePicker'; + +export { default as DateRangePicker } from './DateRangePicker'; +export * from './DateRangePicker'; + +export { + default as DesktopDateRangePicker, + DesktopDateRangePickerProps, +} from './DesktopDateRangePicker'; + +export { + default as MobileDateRangePicker, + MobileDateRangePickerProps, +} from './MobileDateRangePicker'; + +export { + default as StaticDateRangePicker, + StaticDateRangePickerProps, +} from './StaticDateRangePicker'; + +export { default as ClockPicker } from './ClockPicker'; +export * from './ClockPicker'; diff --git a/packages/material-ui-lab/src/index.js b/packages/material-ui-lab/src/index.js index f2ca8bff18413d..90c6b78f5959cb 100644 --- a/packages/material-ui-lab/src/index.js +++ b/packages/material-ui-lab/src/index.js @@ -11,15 +11,75 @@ export * from './Autocomplete'; export { default as AvatarGroup } from './AvatarGroup'; export * from './AvatarGroup'; +export { default as DayPicker } from './DayPicker'; +export * from './DayPicker'; + +export { default as DatePicker } from './DatePicker'; +export * from './DatePicker'; + +export { default as DesktopDatePicker } from './DesktopDatePicker'; +export * from './DesktopDatePicker'; + +export { default as MobileDatePicker } from './MobileDatePicker'; +export * from './MobileDatePicker'; + +export { default as StaticDatePicker } from './StaticDatePicker'; +export * from './StaticDatePicker'; + +export { default as TimePicker } from './TimePicker'; +export * from './TimePicker'; + +export { default as DesktopTimePicker } from './DesktopTimePicker'; +export * from './DesktopTimePicker'; + +export { default as MobileTimePicker } from './MobileTimePicker'; +export * from './MobileTimePicker'; + +export { default as StaticTimePicker } from './StaticTimePicker'; +export * from './StaticTimePicker'; + +export { default as DateTimePicker } from './DateTimePicker'; +export * from './DateTimePicker'; + +export { default as DesktopDateTimePicker } from './DesktopDateTimePicker'; +export * from './DesktopDateTimePicker'; + +export { default as MobileDateTimePicker } from './MobileDateTimePicker'; +export * from './MobileDateTimePicker'; + +export { default as StaticDateTimePicker } from './StaticDateTimePicker'; +export * from './StaticDateTimePicker'; + +export { default as DateRangePicker } from './DateRangePicker'; +export * from './DateRangePicker'; + +export { default as DesktopDateRangePicker } from './DesktopDateRangePicker'; +export * from './DesktopDateRangePicker'; + +export { default as MobileDateRangePicker } from './MobileDateRangePicker'; +export * from './MobileDateRangePicker'; + +export { default as StaticDateRangePicker } from './StaticDateRangePicker'; +export * from './StaticDateRangePicker'; + +export { default as ClockPicker } from './ClockPicker'; +export * from './ClockPicker'; + export { default as LoadingButton } from './LoadingButton'; export * from './LoadingButton'; +export { default as LocalizationProvider } from './LocalizationProvider'; +export * from './LocalizationProvider'; + export { default as Pagination } from './Pagination'; export * from './Pagination'; export { default as PaginationItem } from './PaginationItem'; export * from './PaginationItem'; +export { default as PickersDay } from './PickersDay'; +export * from './PickersDay'; + export { default as Rating } from './Rating'; export * from './Rating'; @@ -68,6 +128,8 @@ export * from './TimelineOppositeContent'; export { default as TimelineSeparator } from './TimelineSeparator'; export * from './TimelineSeparator'; +export * from './TimePicker'; + export { default as ToggleButton } from './ToggleButton'; export * from './ToggleButton'; @@ -80,5 +142,8 @@ export * from './TreeItem'; export { default as TreeView } from './TreeView'; export * from './TreeView'; +export { default as YearPicker } from './YearPicker'; +export * from './YearPicker'; + // createFilterOptions is exported from Autocomplete export { default as useAutocomplete } from './useAutocomplete'; diff --git a/packages/material-ui-lab/src/index.test.js b/packages/material-ui-lab/src/index.test.js index 2b364d08f0a9e6..ea9342587cacf8 100644 --- a/packages/material-ui-lab/src/index.test.js +++ b/packages/material-ui-lab/src/index.test.js @@ -14,7 +14,7 @@ describe('@material-ui/lab', () => { it('should not have undefined exports', () => { Object.keys(MaterialUI).forEach((exportKey) => - expect(Boolean(MaterialUI[exportKey])).to.equal(true), + expect(Boolean(MaterialUI[exportKey]), `${exportKey} is not truthy`).to.equal(true), ); }); }); diff --git a/packages/pickers/lib/src/_shared/KeyboardDateInput.tsx b/packages/material-ui-lab/src/internal/pickers/KeyboardDateInput.tsx similarity index 91% rename from packages/pickers/lib/src/_shared/KeyboardDateInput.tsx rename to packages/material-ui-lab/src/internal/pickers/KeyboardDateInput.tsx index 7855bc95594d6c..45c84a8d476b42 100644 --- a/packages/pickers/lib/src/_shared/KeyboardDateInput.tsx +++ b/packages/material-ui-lab/src/internal/pickers/KeyboardDateInput.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import IconButton from '@material-ui/core/IconButton'; import InputAdornment from '@material-ui/core/InputAdornment'; import { useForkRef } from '@material-ui/core/utils'; import { useUtils } from './hooks/useUtils'; -import { CalendarIcon } from './icons/CalendarIcon'; +import CalendarIcon from '../svg-icons/Calendar'; import { useMaskedInput } from './hooks/useMaskedInput'; import { DateInputProps, DateInputRefs } from './PureDateInput'; -import { getTextFieldAriaText } from '../_helpers/text-field-helper'; +import { getTextFieldAriaText } from './text-field-helper'; export const KeyboardDateInput: React.FC = ({ containerRef, @@ -61,3 +61,5 @@ KeyboardDateInput.propTypes = { renderInput: PropTypes.func.isRequired, rifmFormatter: PropTypes.func, }; + +export default KeyboardDateInput; diff --git a/packages/pickers/lib/src/Picker/Picker.tsx b/packages/material-ui-lab/src/internal/pickers/Picker/Picker.tsx similarity index 55% rename from packages/pickers/lib/src/Picker/Picker.tsx rename to packages/material-ui-lab/src/internal/pickers/Picker/Picker.tsx index 6d4161f95be71b..2e2100b2921069 100644 --- a/packages/pickers/lib/src/Picker/Picker.tsx +++ b/packages/material-ui-lab/src/internal/pickers/Picker/Picker.tsx @@ -1,74 +1,70 @@ import * as React from 'react'; import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import { useViews } from '../_shared/hooks/useViews'; -import { ClockView } from '../views/Clock/ClockView'; -import { DateTimePickerView } from '../DateTimePicker'; -import { BasePickerProps } from '../typings/BasePicker'; -import { DatePickerView } from '../DatePicker/DatePicker'; -import { CalendarView } from '../views/Calendar/CalendarView'; -import { withDefaultProps } from '../_shared/withDefaultProps'; -import { KeyboardDateInput } from '../_shared/KeyboardDateInput'; -import { useIsLandscape } from '../_shared/hooks/useIsLandscape'; +import { styled, createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; +import { useViews } from '../hooks/useViews'; +import ClockPicker from '../../../ClockPicker/ClockPicker'; +import DayPicker from '../../../DayPicker/DayPicker'; +import { KeyboardDateInput } from '../KeyboardDateInput'; +import { useIsLandscape } from '../hooks/useIsLandscape'; import { DIALOG_WIDTH, VIEW_HEIGHT } from '../constants/dimensions'; -import { PickerSelectionState } from '../_shared/hooks/usePickerState'; import { WrapperVariantContext } from '../wrappers/WrapperVariantContext'; -import { MobileKeyboardInputView } from '../views/MobileKeyboardInputView'; -import { - WithViewsProps, - AnyPickerView, - SharedPickerProps, - CalendarAndClockProps, -} from './SharedPickerProps'; - -export interface ExportedPickerProps +import { PickerSelectionState } from '../hooks/usePickerState'; +import { BasePickerProps, CalendarAndClockProps } from '../typings/BasePicker'; +import { WithViewsProps, SharedPickerProps } from './SharedPickerProps'; +import { AllAvailableViews, TimePickerView, DatePickerView } from '../typings/Views'; +import PickerView from './PickerView'; + +export interface ExportedPickerProps extends Omit, CalendarAndClockProps, WithViewsProps { - // TODO move out, cause it is DateTimePickerOnly - hideTabs?: boolean; dateRangeIcon?: React.ReactNode; timeIcon?: React.ReactNode; } export type PickerProps< - TView extends AnyPickerView, + TView extends AllAvailableViews, TInputValue = any, TDateValue = any > = ExportedPickerProps & SharedPickerProps; -const muiComponentConfig = { name: 'MuiPickersBasePicker' }; - -export const useStyles = makeStyles( +export const MobileKeyboardInputView = styled('div')( { - root: { - display: 'flex', - flexDirection: 'column', - }, - landscape: { - flexDirection: 'row', - }, - pickerView: { - overflowX: 'hidden', - width: DIALOG_WIDTH, - maxHeight: VIEW_HEIGHT, - display: 'flex', - flexDirection: 'column', - margin: '0 auto', - }, - pickerViewLandscape: { - padding: '0 8px', - }, + padding: '16px 24px', }, - muiComponentConfig + { name: 'MuiPickersMobileKeyboardInputView' }, ); +export const styles = createStyles({ + root: { + display: 'flex', + flexDirection: 'column', + }, + landscape: { + flexDirection: 'row', + }, + pickerView: { + overflowX: 'hidden', + width: DIALOG_WIDTH, + maxHeight: VIEW_HEIGHT, + display: 'flex', + flexDirection: 'column', + margin: '0 auto', + }, +}); + +export type PickerClassKey = keyof WithStyles['classes']; + const MobileKeyboardTextFieldProps = { fullWidth: true }; -const isDatePickerView = (view: DateTimePickerView) => +const isDatePickerView = (view: AllAvailableViews): view is DatePickerView => view === 'year' || view === 'month' || view === 'date'; +const isTimePickerView = (view: AllAvailableViews): view is TimePickerView => + view === 'hours' || view === 'minutes' || view === 'seconds'; + function Picker({ + classes, className, date, DateInputProps, @@ -84,8 +80,7 @@ function Picker({ toolbarTitle, views = ['year', 'month', 'date', 'hours', 'minutes', 'seconds'], ...other -}: PickerProps) { - const classes = useStyles(); +}: PickerProps & WithStyles) { const isLandscape = useIsLandscape(views, orientation); const wrapperVariant = React.useContext(WrapperVariantContext); @@ -93,20 +88,26 @@ function Picker({ typeof showToolbar === 'undefined' ? wrapperVariant !== 'desktop' : showToolbar; const handleDateChange = React.useCallback( - (date: unknown, selectionState?: PickerSelectionState) => { - onDateChange(date, wrapperVariant, selectionState); + (newDate: unknown, selectionState?: PickerSelectionState) => { + onDateChange(newDate, wrapperVariant, selectionState); }, - [onDateChange, wrapperVariant] + [onDateChange, wrapperVariant], ); const { openView, nextView, previousView, setOpenView, handleChangeAndOpenNext } = useViews({ + view: undefined, views, openTo, onChange: handleDateChange, - isMobileKeyboardViewOpen, - toggleMobileKeyboardView, }); + React.useEffect(() => { + if (isMobileKeyboardViewOpen && toggleMobileKeyboardView) { + toggleMobileKeyboardView(); + } + // React on `openView` change + }, [openView]); // eslint-disable-line + return (
)} -
+ {isMobileKeyboardViewOpen ? ( ) : ( - {(openView === 'year' || openView === 'month' || openView === 'date') && ( - )} - {(openView === 'hours' || openView === 'minutes' || openView === 'seconds') && ( - setOpenView(nextView)} openPreviousView={() => setOpenView(previousView)} @@ -174,9 +169,9 @@ function Picker({ )} )} -
+
); } -export default withDefaultProps(muiComponentConfig, Picker); +export default withStyles(styles, { name: 'MuiPicker' })(Picker); diff --git a/packages/material-ui-lab/src/internal/pickers/Picker/PickerView.tsx b/packages/material-ui-lab/src/internal/pickers/Picker/PickerView.tsx new file mode 100644 index 00000000000000..fb4ef718c084d3 --- /dev/null +++ b/packages/material-ui-lab/src/internal/pickers/Picker/PickerView.tsx @@ -0,0 +1,16 @@ +import { styled } from '@material-ui/core/styles'; +import { DIALOG_WIDTH, VIEW_HEIGHT } from '../constants/dimensions'; + +const PickerView = styled('div')( + { + overflowX: 'hidden', + width: DIALOG_WIDTH, + maxHeight: VIEW_HEIGHT, + display: 'flex', + flexDirection: 'column', + margin: '0 auto', + }, + { name: 'MuiPickerView' }, +); + +export default PickerView; diff --git a/packages/material-ui-lab/src/internal/pickers/Picker/SharedPickerProps.tsx b/packages/material-ui-lab/src/internal/pickers/Picker/SharedPickerProps.tsx new file mode 100644 index 00000000000000..ae066525df032d --- /dev/null +++ b/packages/material-ui-lab/src/internal/pickers/Picker/SharedPickerProps.tsx @@ -0,0 +1,37 @@ +import { BasePickerProps } from '../typings/BasePicker'; +import { ExportedDateInputProps } from '../PureDateInput'; +import { WithDateAdapterProps } from '../withDateAdapterProp'; +import { PickerSelectionState } from '../hooks/usePickerState'; +import { DateInputPropsLike } from '../wrappers/WrapperProps'; +import { AllAvailableViews } from '../typings/Views'; +import { WrapperVariant } from '../wrappers/Wrapper'; + +export type AllSharedPickerProps = BasePickerProps< + TInputValue, + TDateValue +> & + ExportedDateInputProps & + WithDateAdapterProps; + +export interface SharedPickerProps { + isMobileKeyboardViewOpen: boolean; + toggleMobileKeyboardView: () => void; + DateInputProps: TInputProps; + date: TDateValue; + onDateChange: ( + date: TDateValue, + currentWrapperVariant: WrapperVariant, + isFinish?: PickerSelectionState, + ) => void; +} + +export interface WithViewsProps { + /** + * Array of views to show. + */ + views?: T[]; + /** + * First view to show. + */ + openTo?: T; +} diff --git a/packages/pickers/lib/src/Picker/makePickerWithState.tsx b/packages/material-ui-lab/src/internal/pickers/Picker/makePickerWithState.tsx similarity index 77% rename from packages/pickers/lib/src/Picker/makePickerWithState.tsx rename to packages/material-ui-lab/src/internal/pickers/Picker/makePickerWithState.tsx index 88272f3bb3cbc2..c1b84345df0c6a 100644 --- a/packages/pickers/lib/src/Picker/makePickerWithState.tsx +++ b/packages/material-ui-lab/src/internal/pickers/Picker/makePickerWithState.tsx @@ -1,19 +1,21 @@ import * as React from 'react'; import Picker, { ExportedPickerProps } from './Picker'; import { ParsableDate } from '../constants/prop-types'; -import { MuiPickersAdapter } from '../_shared/hooks/useUtils'; -import { parsePickerInputValue } from '../_helpers/date-utils'; -import { withDefaultProps } from '../_shared/withDefaultProps'; -import { KeyboardDateInput } from '../_shared/KeyboardDateInput'; +import { MuiPickersAdapter } from '../hooks/useUtils'; +import { parsePickerInputValue } from '../date-utils'; +import { withDefaultProps } from '../withDefaultProps'; +import { KeyboardDateInput } from '../KeyboardDateInput'; import { SomeWrapper, ExtendWrapper } from '../wrappers/Wrapper'; import { ResponsiveWrapper } from '../wrappers/ResponsiveWrapper'; -import { withDateAdapterProp } from '../_shared/withDateAdapterProp'; +import { withDateAdapterProp } from '../withDateAdapterProp'; import { makeWrapperComponent } from '../wrappers/makeWrapperComponent'; -import { PureDateInput, DateInputProps } from '../_shared/PureDateInput'; -import { usePickerState, PickerStateValueManager } from '../_shared/hooks/usePickerState'; -import { AnyPickerView, AllSharedPickerProps, ToolbarComponentProps } from './SharedPickerProps'; +import { PureDateInput } from '../PureDateInput'; +import { usePickerState, PickerStateValueManager } from '../hooks/usePickerState'; +import { AllAvailableViews } from '../typings/Views'; +import { AllSharedPickerProps } from './SharedPickerProps'; +import { ToolbarComponentProps } from '../typings/BasePicker'; -type AllAvailableForOverrideProps = ExportedPickerProps; +type AllAvailableForOverrideProps = ExportedPickerProps; export type AllPickerProps = T & AllSharedPickerProps & @@ -24,7 +26,7 @@ export interface MakePickerOptions { /** * Hook that running validation for the `value` and input. */ - useValidation: (value: ParsableDate, props: T) => string | null; + useValidation: (value: ParsableDate, props: T) => string | null; /** * Intercept props to override or inject default props specifically for picker. */ @@ -45,29 +47,29 @@ export type SharedPickerProps = ExtendWrapp type PickerComponent< TViewProps extends AllAvailableForOverrideProps, TWrapper extends SomeWrapper -> = (props: TViewProps & SharedPickerProps) => JSX.Element; +> = (props: TViewProps & SharedPickerProps) => JSX.Element; export function makePickerWithStateAndWrapper< T extends AllAvailableForOverrideProps, TWrapper extends SomeWrapper = typeof ResponsiveWrapper >( Wrapper: TWrapper, - { name, useInterceptProps, useValidation, DefaultToolbarComponent }: MakePickerOptions + { name, useInterceptProps, useValidation, DefaultToolbarComponent }: MakePickerOptions, ): PickerComponent { - const WrapperComponent = makeWrapperComponent>(Wrapper, { + const WrapperComponent = makeWrapperComponent(Wrapper, { KeyboardDateInputComponent: KeyboardDateInput, PureDateInputComponent: PureDateInput, }); function PickerWithState( - __props: T & AllSharedPickerProps, TDate> & ExtendWrapper + __props: T & AllSharedPickerProps, TDate> & ExtendWrapper, ) { const allProps = useInterceptProps(__props) as AllPickerProps; const validationError = useValidation(allProps.value, allProps) !== null; const { pickerProps, inputProps, wrapperProps } = usePickerState, TDate>( allProps, - valueManager as PickerStateValueManager, TDate> + valueManager as PickerStateValueManager, TDate>, ); // Note that we are passing down all the value without spread. @@ -90,10 +92,11 @@ export function makePickerWithStateAndWrapper< const FinalPickerComponent = withDefaultProps({ name }, withDateAdapterProp(PickerWithState)); + // tslint:disable-next-line // @ts-ignore Simply ignore generic values in props, because it is impossible // to keep generics without additional cast when using forwardRef // @see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35834 return React.forwardRef>( - (props, ref) => + (props, ref) => , ); } diff --git a/packages/pickers/lib/src/_shared/ArrowSwitcher.tsx b/packages/material-ui-lab/src/internal/pickers/PickersArrowSwitcher.tsx similarity index 78% rename from packages/pickers/lib/src/_shared/ArrowSwitcher.tsx rename to packages/material-ui-lab/src/internal/pickers/PickersArrowSwitcher.tsx index 1d336d561b62eb..4c21178585629a 100644 --- a/packages/pickers/lib/src/_shared/ArrowSwitcher.tsx +++ b/packages/material-ui-lab/src/internal/pickers/PickersArrowSwitcher.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import clsx from 'clsx'; import Typography from '@material-ui/core/Typography'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; +import { createStyles, WithStyles, withStyles, Theme, useTheme } from '@material-ui/core/styles'; import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; -import { ArrowLeftIcon } from './icons/ArrowLeft'; -import { ArrowRightIcon } from './icons/ArrowRight'; +import ArrowLeftIcon from '../svg-icons/ArrowLeft'; +import ArrowRightIcon from '../svg-icons/ArrowRight'; export interface ExportedArrowSwitcherProps { /** @@ -25,12 +25,10 @@ export interface ExportedArrowSwitcherProps { rightArrowButtonText?: string; /** * Props to pass to left arrow button. - * @type {Partial} */ leftArrowButtonProps?: Partial; /** * Props to pass to right arrow button. - * @type {Partial} */ rightArrowButtonProps?: Partial; } @@ -45,8 +43,8 @@ interface ArrowSwitcherProps extends ExportedArrowSwitcherProps, React.HTMLProps text?: string; } -export const useStyles = makeStyles( - (theme) => ({ +export const styles = (theme: Theme) => + createStyles({ root: {}, iconButton: { zIndex: 1, @@ -58,12 +56,16 @@ export const useStyles = makeStyles( hidden: { visibility: 'hidden', }, - }), - { name: 'MuiPickersArrowSwitcher' } -); + }); + +export type PickersArrowSwitcherClassKey = keyof WithStyles['classes']; -const PureArrowSwitcher = React.forwardRef((props, ref) => { +const PickersArrowSwitcher = React.forwardRef< + HTMLDivElement, + ArrowSwitcherProps & WithStyles +>((props, ref) => { const { + classes, className, isLeftDisabled, isLeftHidden, @@ -80,15 +82,14 @@ const PureArrowSwitcher = React.forwardRef(( text, ...other } = props; - const classes = useStyles(); const theme = useTheme(); const isRtl = theme.direction === 'rtl'; return (
(( )} {isRtl ? leftArrowIcon : rightArrowIcon} @@ -122,6 +123,6 @@ const PureArrowSwitcher = React.forwardRef(( ); }); -PureArrowSwitcher.displayName = 'ArrowSwitcher'; - -export const ArrowSwitcher = React.memo(PureArrowSwitcher); +export default withStyles(styles, { name: 'MuiPickersArrowSwitcher' })( + React.memo(PickersArrowSwitcher), +); diff --git a/packages/pickers/lib/src/_shared/PickersModalDialog.tsx b/packages/material-ui-lab/src/internal/pickers/PickersModalDialog.tsx similarity index 63% rename from packages/pickers/lib/src/_shared/PickersModalDialog.tsx rename to packages/material-ui-lab/src/internal/pickers/PickersModalDialog.tsx index 7e1d838aa4b328..c941c7acd83f4e 100644 --- a/packages/pickers/lib/src/_shared/PickersModalDialog.tsx +++ b/packages/material-ui-lab/src/internal/pickers/PickersModalDialog.tsx @@ -3,100 +3,100 @@ import clsx from 'clsx'; import Button from '@material-ui/core/Button'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; -import Dialog, { DialogProps } from '@material-ui/core/Dialog'; -import { makeStyles } from '@material-ui/core/styles'; -import { DIALOG_WIDTH, DIALOG_WIDTH_WIDER } from '../constants/dimensions'; +import Dialog, { DialogProps as MuiDialogProps } from '@material-ui/core/Dialog'; +import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; +import { DIALOG_WIDTH, DIALOG_WIDTH_WIDER } from './constants/dimensions'; export interface ExportedPickerModalProps { /** * "OK" button text. - * * @default "OK" */ okText?: React.ReactNode; /** * "CANCEL" Text message - * * @default "CANCEL" */ cancelText?: React.ReactNode; /** * "CLEAR" Text message - * * @default "CLEAR" */ clearText?: React.ReactNode; /** * "TODAY" Text message - * * @default "TODAY" */ todayText?: React.ReactNode; /** * If `true`, it shows the clear action in the picker dialog. - * * @default false */ clearable?: boolean; /** * If `true`, the today button will be displayed. **Note** that `showClearButton` has a higher priority. - * * @default false */ showTodayButton?: boolean; - showTabs?: boolean; - wider?: boolean; + /** + * Props to be passed directly to material-ui [Dialog](https://material-ui.com/components/dialogs) + */ + DialogProps?: Partial; } -export interface PickerModalDialogProps extends ExportedPickerModalProps, DialogProps { +export interface PickerModalDialogProps extends ExportedPickerModalProps { onAccept: () => void; onClear: () => void; onDismiss: () => void; onSetToday: () => void; + wider?: boolean; + open: boolean; } -export const useStyles = makeStyles( - { - dialogRoot: { - minWidth: DIALOG_WIDTH, - }, - dialogRootWider: { - minWidth: DIALOG_WIDTH_WIDER, - }, - dialogContainer: { - '&:focus > $dialogRoot': { - outline: 'auto', - '@media (pointer:coarse)': { - outline: 0, - }, - }, - }, - dialog: { - '&:first-child': { - padding: 0, +export const styles = createStyles({ + dialogRoot: { + minWidth: DIALOG_WIDTH, + }, + dialogRootWider: { + minWidth: DIALOG_WIDTH_WIDER, + }, + dialogContainer: { + '&:focus > $dialogRoot': { + outline: 'auto', + '@media (pointer:coarse)': { + outline: 0, }, }, - dialogAction: { - // requested for overrides + }, + dialog: { + '&:first-child': { + padding: 0, }, - withAdditionalAction: { - // set justifyContent to default value to fix IE11 layout bug - // see https://github.com/mui-org/material-ui-pickers/pull/267 - justifyContent: 'flex-start', + }, + dialogAction: { + // requested for overrides + }, + withAdditionalAction: { + // set justifyContent to default value to fix IE11 layout bug + // see https://github.com/mui-org/material-ui-pickers/pull/267 + justifyContent: 'flex-start', - '& > *:first-child': { - marginRight: 'auto', - }, + '& > *:first-child': { + marginRight: 'auto', }, }, - { name: 'MuiPickersModalDialog' } -); +}); + +export type PickersModalDialogClassKey = keyof WithStyles['classes']; -const PickersModalDialog: React.FC = (props) => { +const PickersModalDialog: React.FC> = ( + props, +) => { const { + open, + classes, cancelText = 'Cancel', children, - classes: MuiDialogClasses, clearable = false, clearText = 'Clear', okText = 'OK', @@ -104,16 +104,16 @@ const PickersModalDialog: React.FC = (props) => { onClear, onDismiss, onSetToday, - showTabs, showTodayButton = false, todayText = 'Today', wider, - ...other + DialogProps, } = props; - const classes = useStyles(); + const MuiDialogClasses = DialogProps?.classes; return ( = (props) => { }), ...MuiDialogClasses, }} - {...other} + {...DialogProps} > {children} = (props) => { ); }; -export default PickersModalDialog; +export default withStyles(styles, { name: 'MuiPickersModalDialog' })(PickersModalDialog); diff --git a/packages/pickers/lib/src/_shared/PickersPopper.tsx b/packages/material-ui-lab/src/internal/pickers/PickersPopper.tsx similarity index 62% rename from packages/pickers/lib/src/_shared/PickersPopper.tsx rename to packages/material-ui-lab/src/internal/pickers/PickersPopper.tsx index b25d721184026d..a3537b89b546dc 100644 --- a/packages/pickers/lib/src/_shared/PickersPopper.tsx +++ b/packages/material-ui-lab/src/internal/pickers/PickersPopper.tsx @@ -1,38 +1,41 @@ import * as React from 'react'; import clsx from 'clsx'; import Grow from '@material-ui/core/Grow'; -import Paper, { PaperProps } from '@material-ui/core/Paper'; -import Popper, { PopperProps } from '@material-ui/core/Popper'; -import TrapFocus, { TrapFocusProps } from '@material-ui/core/Unstable_TrapFocus'; -import { useForkRef } from '@material-ui/core/utils'; -import { makeStyles } from '@material-ui/core/styles'; -import { TransitionProps } from '@material-ui/core/transitions'; +import Paper, { PaperProps as MuiPaperProps } from '@material-ui/core/Paper'; +import Popper, { PopperProps as MuiPopperProps } from '@material-ui/core/Popper'; +import TrapFocus, { + TrapFocusProps as MuiTrapFocusProps, +} from '@material-ui/core/Unstable_TrapFocus'; +import { useForkRef, setRef, useEventCallback } from '@material-ui/core/utils'; +import { createStyles, WithStyles, withStyles, Theme } from '@material-ui/core/styles'; +import { TransitionProps as MuiTransitionProps } from '@material-ui/core/transitions'; import { useGlobalKeyDown, keycode } from './hooks/useKeyDown'; -import { IS_TOUCH_DEVICE_MEDIA } from '../constants/dimensions'; -import { executeInTheNextEventLoopTick } from '../_helpers/utils'; +import { IS_TOUCH_DEVICE_MEDIA } from './constants/dimensions'; +import { executeInTheNextEventLoopTick } from './utils'; export interface ExportedPickerPopperProps { /** * Popper props passed down to [Popper](https://material-ui.com/api/popper/) component. */ - PopperProps?: Partial; + PopperProps?: Partial; /** - * Custom component for [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). + * Custom component for popper [Transition](https://material-ui.com/components/transitions/#transitioncomponent-prop). */ - TransitionComponent?: React.ComponentType; + TransitionComponent?: React.ComponentType; } -export interface PickerPopperProps extends ExportedPickerPopperProps, PaperProps { +export interface PickerPopperProps extends ExportedPickerPopperProps, MuiPaperProps { role: 'tooltip' | 'dialog'; - TrapFocusProps?: Partial; - anchorEl: PopperProps['anchorEl']; - open: PopperProps['open']; + TrapFocusProps?: Partial; + anchorEl: MuiPopperProps['anchorEl']; + open: MuiPopperProps['open']; + containerRef?: React.Ref; onClose: () => void; onOpen: () => void; } -export const useStyles = makeStyles( - (theme) => ({ +export const styles = (theme: Theme) => + createStyles({ root: { zIndex: theme.zIndex.modal, }, @@ -47,15 +50,16 @@ export const useStyles = makeStyles( topTransition: { transformOrigin: 'bottom center', }, - }), - { name: 'MuiPickersPopper' } -); + }); + +export type PickersPopperClassKey = keyof WithStyles['classes']; -export const PickersPopper: React.FC = (props) => { +const PickersPopper: React.FC> = (props) => { const { anchorEl, children, - innerRef = null, + classes, + containerRef = null, onClose, onOpen, open, @@ -64,11 +68,17 @@ export const PickersPopper: React.FC = (props) => { TransitionComponent = Grow, TrapFocusProps, } = props; - const classes = useStyles(); const paperRef = React.useRef(null); - const handlePopperRef = useForkRef(paperRef, innerRef); + const handleRef = useForkRef(paperRef, containerRef); const lastFocusedElementRef = React.useRef(null); - const popperOptions = React.useMemo(() => ({ onCreate: onOpen }), [onOpen]); + + const handlePaperRef = useEventCallback((node) => { + setRef(handleRef, node); + + if (node) { + onOpen(); + } + }); useGlobalKeyDown(open, { [keycode.Esc]: onClose, @@ -111,7 +121,6 @@ export const PickersPopper: React.FC = (props) => { open={open} anchorEl={anchorEl} className={clsx(classes.root, PopperProps?.className)} - popperOptions={popperOptions} {...PopperProps} > {({ TransitionProps, placement }) => ( @@ -127,7 +136,7 @@ export const PickersPopper: React.FC = (props) => { = (props) => { ); }; + +export default withStyles(styles, { name: 'MuiPickersPopper' })(PickersPopper); diff --git a/packages/pickers/lib/src/_shared/PickerToolbar.tsx b/packages/material-ui-lab/src/internal/pickers/PickersToolbar.tsx similarity index 62% rename from packages/pickers/lib/src/_shared/PickerToolbar.tsx rename to packages/material-ui-lab/src/internal/pickers/PickersToolbar.tsx index 306df4f3aba8ac..0ab9f8efebd1d2 100644 --- a/packages/pickers/lib/src/_shared/PickerToolbar.tsx +++ b/packages/material-ui-lab/src/internal/pickers/PickersToolbar.tsx @@ -3,46 +3,44 @@ import clsx from 'clsx'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; -import { makeStyles } from '@material-ui/core/styles'; +import { createStyles, WithStyles, withStyles, Theme } from '@material-ui/core/styles'; import Toolbar, { ToolbarProps } from '@material-ui/core/Toolbar'; -import { ExtendMui } from '../typings/helpers'; -import { PenIcon } from './icons/Pen'; -import { CalendarIcon } from './icons/CalendarIcon'; -import { ToolbarComponentProps } from '../Picker/SharedPickerProps'; +import { ExtendMui } from './typings/helpers'; +import PenIcon from '../svg-icons/Pen'; +import CalendarIcon from '../svg-icons/Calendar'; +import { ToolbarComponentProps } from './typings/BasePicker'; -export const useStyles = makeStyles( - (theme) => { - const toolbarBackground = - theme.palette.type === 'light' - ? theme.palette.primary.main - : theme.palette.background.default; - return { - root: { - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - justifyContent: 'space-between', - paddingTop: 16, - paddingBottom: 16, - backgroundColor: toolbarBackground, - color: theme.palette.getContrastText(toolbarBackground), - }, - toolbarLandscape: { - height: 'auto', - maxWidth: 160, - padding: 16, - justifyContent: 'flex-start', - flexWrap: 'wrap', - }, - dateTitleContainer: { - flex: 1, - }, - }; - }, - { name: 'MuiPickersToolbar' } -); +export const styles = (theme: Theme) => { + const toolbarBackground = + theme.palette.mode === 'light' ? theme.palette.primary.main : theme.palette.background.default; -interface PickerToolbarProps + return createStyles({ + root: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'space-between', + paddingTop: 16, + paddingBottom: 16, + backgroundColor: toolbarBackground, + color: theme.palette.getContrastText(toolbarBackground), + }, + toolbarLandscape: { + height: 'auto', + maxWidth: 160, + padding: 16, + justifyContent: 'flex-start', + flexWrap: 'wrap', + }, + dateTitleContainer: { + flex: 1, + }, + }); +}; + +export type PickersToolbarClassKey = keyof WithStyles['classes']; + +export interface PickersToolbarProps extends ExtendMui, Pick< ToolbarComponentProps, @@ -62,8 +60,9 @@ function defaultGetKeyboardInputSwitchingButtonText(isKeyboardInputOpen: boolean : 'calendar view is open, go to text input view'; } -const PickerToolbar: React.SFC = ({ +const PickerToolbar: React.FC> = ({ children, + classes, className, getMobileKeyboardInputViewButtonText = defaultGetKeyboardInputSwitchingButtonText, isLandscape, @@ -73,8 +72,6 @@ const PickerToolbar: React.SFC = ({ toggleMobileKeyboardView, toolbarTitle, }) => { - const classes = useStyles(); - return ( = ({ ); }; -export default PickerToolbar; +export default withStyles(styles, { name: 'MuiPickersToolbar' })(PickerToolbar); diff --git a/packages/pickers/lib/src/_shared/ToolbarButton.tsx b/packages/material-ui-lab/src/internal/pickers/PickersToolbarButton.tsx similarity index 52% rename from packages/pickers/lib/src/_shared/ToolbarButton.tsx rename to packages/material-ui-lab/src/internal/pickers/PickersToolbarButton.tsx index b41e74bb7b38d1..99c028ad8a5cfd 100644 --- a/packages/pickers/lib/src/_shared/ToolbarButton.tsx +++ b/packages/material-ui-lab/src/internal/pickers/PickersToolbarButton.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import clsx from 'clsx'; import Button, { ButtonProps } from '@material-ui/core/Button'; -import { makeStyles } from '@material-ui/core/styles'; +import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles'; import { TypographyProps } from '@material-ui/core/Typography'; -import ToolbarText from './ToolbarText'; -import { ExtendMui } from '../typings/helpers'; +import ToolbarText from './PickersToolbarText'; +import { ExtendMui } from './typings/helpers'; export interface ToolbarButtonProps extends ExtendMui { align?: TypographyProps['align']; @@ -14,20 +14,29 @@ export interface ToolbarButtonProps extends ExtendMui = (props) => { - const { align, className, selected, typographyClassName, value, variant, ...other } = props; - const classes = useStyles(); +export type PickersToolbarButtonClassKey = keyof WithStyles['classes']; + +const ToolbarButton: React.FunctionComponent> = ( + props, +) => { + const { + align, + classes, + className, + selected, + typographyClassName, + value, + variant, + ...other + } = props; return ( - - - ); - } - - return ( - - - - {children} - - - ); - } -} - -NavItem.propTypes = { - classes: PropTypes.object.isRequired, - open: PropTypes.bool.isRequired, - href: PropTypes.string, - title: PropTypes.string.isRequired, - children: PropTypes.arrayOf(PropTypes.object), - depth: PropTypes.number, -}; - -NavItem.defaultProps = { - depth: 0, -}; - -export default withStyles(styles)(withRouter(NavItem)); diff --git a/packages/pickers/docs/layout/components/NavigationMenu.tsx b/packages/pickers/docs/layout/components/NavigationMenu.tsx deleted file mode 100644 index 90f2719f0678d6..00000000000000 --- a/packages/pickers/docs/layout/components/NavigationMenu.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import NavItem from './NavItem'; -import { withRouter } from 'next/router'; -import { List } from '@material-ui/core'; -import { navItems } from './navigationMap'; -import { stringToTestId } from 'utils/helpers'; - -class NavigationMenu extends React.Component { - mapNavigation(depth: number) { - return ({ title, children, href, as }: any) => { - const { asPath } = this.props.router; - const hasChildren = children && children.length > 0; - const open = hasChildren - ? children.some((item: any) => item.href === asPath || item.as === asPath) - : false; - - return ( - - {children && children.length > 0 && children.map(this.mapNavigation(depth + 1))} - - ); - }; - } - - render() { - return {navItems.map(this.mapNavigation(0))}; - } -} - -export default withRouter(NavigationMenu); diff --git a/packages/pickers/docs/layout/components/navigationMap.ts b/packages/pickers/docs/layout/components/navigationMap.ts deleted file mode 100644 index d2b9c535f4c5b2..00000000000000 --- a/packages/pickers/docs/layout/components/navigationMap.ts +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypesDoc from '../../prop-types.json'; - -export const navItems = [ - { - title: 'Getting Started', - children: [ - { title: 'Installation', href: '/getting-started/installation' }, - { title: 'Usage', href: '/getting-started/usage' }, - { title: 'Parsing dates', href: '/getting-started/parsing' }, - ], - }, - { - title: 'Localization', - children: [ - { title: 'Using date-fns', href: '/localization/date-fns' }, - { title: 'Using moment', href: '/localization/moment' }, - { title: 'Additional Calendar Systems', href: '/localization/calendar-systems' }, - ], - }, - { - title: 'Components Demo', - children: [ - { title: 'Date Picker', href: '/demo/datepicker' }, - { title: 'Date Range Picker', href: '/demo/daterangepicker' }, - { title: 'Time Picker', href: '/demo/timepicker' }, - { title: 'Date & Time Picker', href: '/demo/datetime-picker' }, - ], - }, - { - title: 'Components API', - children: Object.keys(PropTypesDoc) - .filter((component) => !component.match(/^(Mobile|Desktop|Static)/)) - .map((component) => ({ - title: component, - as: `/api/${component}`, - href: `/api/props?component=${component}`, - })), - }, - { - title: 'Guides', - children: [ - { title: 'TypeScript', href: '/guides/typescript' }, - { title: 'Accessibility', href: '/guides/accessibility' }, - { title: 'Form integration', href: '/guides/forms' }, - { title: 'CSS overrides', href: '/guides/css-overrides' }, - { title: 'Passing date adapter', href: '/guides/date-adapter-passing' }, - { title: 'Date management customization', href: '/guides/date-io-customization' }, - { - title: 'Open pickers programmatically', - href: '/guides/controlling-programmatically', - }, - { title: 'Static inner components', href: '/guides/static-components' }, - { title: 'Updating to v3', href: '/guides/upgrading-to-v3' }, - ], - }, -] as const; diff --git a/packages/pickers/docs/layout/styleOverrides.ts b/packages/pickers/docs/layout/styleOverrides.ts deleted file mode 100644 index cab7c407554c40..00000000000000 --- a/packages/pickers/docs/layout/styleOverrides.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Theme } from '@material-ui/core'; -import { StyleRules } from '@material-ui/core/styles'; - -export const createOverrides = (theme: Theme): StyleRules => ({ - body: { - fontFamily: 'roboto', - '-webkit-font-smoothing:': 'antialiased', - backgroundColor: theme.palette.background.default, - }, - h1: theme.typography.h1, - h2: { - ...theme.typography.h2, - textTransform: 'unset', - margin: '32px 0 16px', - }, - h3: { - ...theme.typography.h3, - textTransform: 'unset', - margin: '32px 0 16px', - }, - h4: { - ...theme.typography.h4, - textTransform: 'unset', - margin: '32px 0 8px', - }, - h5: theme.typography.h5, - h6: theme.typography.h6, - p: { - ...theme.typography.body1, - textTransform: 'unset', - }, - a: { - color: theme.palette.secondary.main, - }, - pre: { - margin: '24px 0', - padding: '12px 18px', - overflow: 'auto', - borderRadius: 4, - backgroundColor: theme.palette.background.paper + ' !important', - }, - ul: { - color: theme.palette.text.primary, - }, - li: theme.typography.body1, - '.mui-pickers-markdown-table': { - boxShadow: theme.shadows[3], - backgroundColor: theme.palette.background.paper, - borderCollapse: 'collapse', - - '& th, td': { - padding: 16, - ...theme.typography.body1, - border: `1px solid ${theme.palette.divider}`, - }, - - '& th': { - ...theme.typography.h6, - }, - }, - code: { - fontSize: 16, - lineHeight: 1.4, - fontFamily: "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace", - color: theme.palette.type === 'dark' ? theme.palette.text.primary : 'black', - whiteSpace: 'pre', - wordSpacing: 'normal', - wordBreak: 'normal', - wordWrap: 'normal', - backgroundColor: theme.palette.background.paper + ' !important', - }, - blockquote: { - marginLeft: 0, - paddingLeft: '1em', - borderLeft: `3px solid ${theme.palette.grey[200]}`, - - '& > p': { - fontSize: '0.9rem', - }, - }, - 'h1, h2, h3, h4, h5': { - position: 'relative', - '& a.anchor-link': { - position: 'absolute', - top: -80, - }, - '& a.anchor-link-style': { - visibility: 'hidden', - marginLeft: 4, - fontSize: '80%', - textDecoration: 'none', - color: theme.palette.text.secondary, - fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto', - }, - '&:hover a.anchor-link-style': { - visibility: 'visible', - }, - }, -}); diff --git a/packages/pickers/docs/loaders/example-loader.js b/packages/pickers/docs/loaders/example-loader.js deleted file mode 100644 index 8883e1ba1c2f55..00000000000000 --- a/packages/pickers/docs/loaders/example-loader.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable */ -const path = require('path'); -const safeJsonStringify = require('safe-json-stringify'); - -const root = path.resolve(__dirname, '..'); - -module.exports = function exampleLoader(source) { - const relativePath = path.relative(root, this.resource); - - const escapedRawSource = safeJsonStringify(source.replace(/'/g, '"')); - const sourceWithExportedContext = - source + - `\nexport const raw = ${escapedRawSource}` + - `\nexport const relativePath = "${relativePath}"`; - - return sourceWithExportedContext; -}; diff --git a/packages/pickers/docs/next.config.js b/packages/pickers/docs/next.config.js deleted file mode 100644 index b6d2070614550b..00000000000000 --- a/packages/pickers/docs/next.config.js +++ /dev/null @@ -1,82 +0,0 @@ -const path = require('path'); -const withCSS = require('@zeit/next-css'); -const withImages = require('next-images'); -const withTypescript = require('@zeit/next-typescript'); -const rehypePrism = require('@mapbox/rehype-prism'); -const withTM = require('next-transpile-modules'); -const slug = require('remark-slug'); -const webpack = require('webpack'); -const withBundleAnalyzer = require('@zeit/next-bundle-analyzer'); -const headings = require('./utils/anchor-autolink'); -const tableStyler = require('./utils/table-styler'); - -// eslint-disable-next-line import/order -const withMDX = require('@zeit/next-mdx')({ - extension: /\.(md|mdx)?$/, - options: { - hastPlugins: [rehypePrism], - mdPlugins: [slug, headings, tableStyler], - }, -}); - -module.exports = withBundleAnalyzer( - withCSS( - withImages( - withTypescript( - withMDX( - withTM({ - webpack: (config) => { - if (config.optimization.splitChunks.cacheGroups) { - // split all date libs to separate chunk - config.optimization.splitChunks.cacheGroups.dateLibs = { - name: 'commons', - chunks: 'all', - test: /(luxon|moment|date-fns|dayjs)/, - }; - // move all pickers code to not duplicate it in each chunk - config.optimization.splitChunks.cacheGroups.pickers = { - name: 'pickers', - chunks: 'all', - test: /[\\/]node_modules[\\/]@material-ui\/pickers[\\/]/, - }; - } - - // Process examples to inject raw code strings - config.module.rules.push({ - test: /\.example\.(js|jsx|tsx|ts)$/, - include: [path.resolve(__dirname, 'pages')], - use: [ - { loader: 'next-babel-loader' }, - { - loader: path.resolve(__dirname, 'loaders', 'example-loader.js'), - }, - ], - }); - - // Resolve roots also for mdx pages - config.resolve.modules.push(__dirname); - config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)); - - return config; - }, - target: process.env.IS_NOW ? 'serverless' : 'server', - pageExtensions: ['ts', 'tsx', 'md', 'mdx'], - transpileModules: ['@material-ui/pickers'], - analyzeServer: ['server', 'both'].includes(process.env.BUNDLE_ANALYZE), - analyzeBrowser: ['browser', 'both'].includes(process.env.BUNDLE_ANALYZE), - bundleAnalyzerConfig: { - server: { - analyzerMode: 'static', - reportFilename: '../../.next/bundle/server.html', - }, - browser: { - analyzerMode: 'static', - reportFilename: '../.next/bundle/client.html', - }, - }, - }) - ) - ) - ) - ) -); diff --git a/packages/pickers/docs/notifications.json b/packages/pickers/docs/notifications.json deleted file mode 100644 index fe51488c7066f6..00000000000000 --- a/packages/pickers/docs/notifications.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/packages/pickers/docs/package.json b/packages/pickers/docs/package.json deleted file mode 100644 index ff0b1d72aa5383..00000000000000 --- a/packages/pickers/docs/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "docs", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "dev": "next dev --port 3001", - "build": "next build", - "start": "next start -p 3001", - "generate-backers": "node scripts/generate-backers.js", - "build:typescript": "tsc" - }, - "keywords": [], - "author": "", - "license": "ISC", - "engines": { - "node": ">=8.0.0" - }, - "dependencies": { - "@babel/core": "^7.9.6", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-proposal-optional-chaining": "^7.9.0", - "@date-io/hijri": "^2.6.0", - "@date-io/jalaali": "^2.6.0", - "@mapbox/rehype-prism": "^0.4.0", - "@material-ui/core": "^5.0.0-alpha.5", - "@material-ui/icons": "^5.0.0-alpha.4", - "@material-ui/lab": "^5.0.0-alpha.5", - "@material-ui/pickers": "^4.0.0-alpha.1", - "@mdx-js/mdx": "^0.15.7", - "@now/node": "^1.7.3", - "@types/fuzzy-search": "^2.1.0", - "@types/isomorphic-fetch": "^0.0.35", - "@types/jss": "^10.0.0", - "@types/luxon": "^1.24.3", - "@types/moment-jalaali": "^0.7.4", - "@types/next": "^8.0.1", - "@types/prismjs": "^1.16.1", - "@types/react": "^16.9.35", - "@types/react-kawaii": "^0.11.0", - "@types/sinon": "^9.0.4", - "@types/yup": "^0.29.2", - "@zeit/next-bundle-analyzer": "^0.1.2", - "@zeit/next-css": "^1.0.1", - "@zeit/next-mdx": "^1.2.0", - "@zeit/next-typescript": "^1.1.1", - "babel-plugin-module-resolver": "^4.0.0", - "clsx": "^1.0.2", - "date-fns": "^2.12.0", - "dayjs": "^1.8.27", - "formik": "^2.1.4", - "fuzzy-search": "^3.2.1", - "isomorphic-fetch": "^2.2.1", - "jss": "^10.3.0", - "jss-rtl": "^0.3.0", - "luxon": "^1.23.0", - "material-ui-search-bar": "^1.0.0-beta.13", - "moment": "^2.27.0", - "moment-hijri": "^2.1.2", - "moment-jalaali": "^0.9.2", - "next": "8.0.4", - "next-cookies": "2.0.3", - "next-images": "^1.4.1", - "next-transpile-modules": "^2.0.0", - "notistack": "^0.9.17", - "now": "^19.1.1", - "prismjs": "^1.21.0", - "raw-loader": "^1.0.0", - "react": "^16.13.0", - "react-docgen-typescript": "^1.17.0", - "react-dom": "^16.13.0", - "react-kawaii": "^0.16.0", - "react-markdown": "^4.3.1", - "remark-slug": "^6.0.0", - "safe-json-stringify": "^1.2.0", - "sinon": "^9.0.2", - "styled-jsx": "^3.3.0", - "typescript": "^3.9.6", - "webpack": "^4.43.0", - "yup": "^0.29.1" - }, - "devDependencies": { - "dotenv": "^8.2.0", - "eslint-plugin-react": "^7.19.0", - "fs-extra": "^9.0.0", - "now": "^19.1.1", - "patreon": "^0.4.1" - } -} diff --git a/packages/pickers/docs/pages/_app.tsx b/packages/pickers/docs/pages/_app.tsx deleted file mode 100644 index 12d48064f1dac0..00000000000000 --- a/packages/pickers/docs/pages/_app.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import sinon from 'sinon'; -import React from 'react'; -import App from 'next/app'; -import cookies from 'next-cookies'; -import getPageContext from '../utils/getPageContext'; -import { PageWithContexts, ThemeType } from '../layout/PageWithContext'; - -if (process.env.VISUAL_TESTING) { - const now = new Date('2019-01-01T09:41:00.000Z'); - sinon.useFakeTimers(now.getTime()); -} - -class MyApp extends App<{ theme: ThemeType }> { - pageContext = getPageContext(); - - static async getInitialProps({ Component, ctx }: any) { - let pageProps = {}; - const { theme } = cookies(ctx); - - if (Component.getInitialProps) { - pageProps = await Component.getInitialProps(ctx); - } - - return { theme, pageProps }; - } - - componentDidMount() { - // Remove the server-side injected CSS. - const jssStyles = document.querySelector('#jss-server-side'); - if (jssStyles && jssStyles.parentNode) { - jssStyles.parentNode.removeChild(jssStyles); - } - } - - render() { - const { Component, pageProps, theme } = this.props; - - return ( - - - - ); - } -} - -export default MyApp; diff --git a/packages/pickers/docs/pages/_document.tsx b/packages/pickers/docs/pages/_document.tsx deleted file mode 100644 index dce92f8d507901..00000000000000 --- a/packages/pickers/docs/pages/_document.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable */ -import React from 'react'; -import PropTypes from 'prop-types'; -import cookies from 'next-cookies'; -// @ts-ignore -import flush from 'styled-jsx/server'; -import Document, { Head, Main, NextScript, NextDocumentContext } from 'next/document'; -import { prismThemes } from '../utils/prism'; -import { ThemeType } from 'layout/PageWithContext'; -import { PageContext } from '../utils/getPageContext'; - -class MyDocument extends Document<{ theme?: ThemeType }> { - static getInitialProps = (ctx: NextDocumentContext) => { - // Render app and page and get the context of the page with collected side effects. - let pageContext: PageContext | undefined; - - const { theme } = cookies(ctx as any); - const page = ctx.renderPage((Component) => { - const WrappedComponent = (props: any) => { - pageContext = props.pageContext; - return ; - }; - - WrappedComponent.propTypes = { - pageContext: PropTypes.object.isRequired, - }; - - return WrappedComponent; - }); - - let css: string; - // It might be undefined, e.g. after an error. - if (pageContext) { - css = pageContext.sheetsRegistry.toString(); - } - - return { - ...page, - theme, - pageContext, - // Styles fragment is rendered after the app and page rendering finish. - styles: ( - -