From 0b069d97fd6e9a9376267c53fab811098dd05b88 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sun, 9 Aug 2020 18:20:21 +0200 Subject: [PATCH] [DatePicker] Squash compat commits Move picker sources into lab (#4) Update README.md [DatePicker] Refactor pickers tests to testing-library and mocha (#5) [TimePicker] Migrate tests to testing library (#8) [DateTimePicker] Migrate tests (#9) Fix all pickers linter errors (#10) Fix all circular dependencies (#11) * Fix all circular dependencies * Enable mocha eslint rules for typescript tests [test] The last step to a green CI (#15) Migrate pickers docs (#12) Downgrade to withStyles for pickers sources (#16) Add public api exports for pickers components (#17) Consolidate component namespace and theme augmentation (#18) Describe conformance for pickers sub-components (#19) Autogenerate proptypes for typescript sources (#20) Proper build output (#21) Clear migration artifacts (#23) Eslint rule for lower-case test name convention (#24) DateRangePicker (#25) yarn deduplicate Remove GridListTile [DateTimePicker] Fix migration unit tests Fix types Fix typescript types migration issues Fix yarn lerna build (#33) Fix karma tests use window.Touch for CI karma tests Remove more outdated diff noise (#34) Replace not valid formats with valid ISO strings Try to fix CI touch tests Skip tests if Touch events are not supported Fix merge conflicts Actually type-check Fix safari tests Remove lowercase test name rule The casing is up to the test author. We're not the grammar police in tests. Fix lint Format Remove overzealous eslint-disable* Debug failing tests Better debugging Timezones are fun was isoString th efix? Let's find out what's failing and then skip it Branch for safari Skip DateRangePicker in browsers review Matt's review Co-authored-by: Matt format docs:i18n --- .eslintrc.js | 3 +- .gitignore | 1 + docs/next.config.js | 6 +- docs/package.json | 3 +- docs/pages/components/date-picker.js | 24 + docs/pages/components/date-range-picker.js | 28 + docs/pages/components/date-time-picker.js | 28 + docs/pages/components/time-picker.js | 24 + docs/scripts/buildApi.ts | 4 + docs/src/modules/utils/helpers.js | 18 +- docs/src/modules/utils/helpers.test.js | 52 +- docs/src/pages.js | 13 +- .../components/date-picker/BasicDatePicker.js | 22 + .../date-picker/BasicDatePicker.tsx | 22 + .../pages/components/date-picker/CustomDay.js | 78 + .../components/date-picker/CustomDay.tsx | 65 +- .../components/date-picker/CustomInput.js | 27 + .../components/date-picker/CustomInput.tsx | 27 + .../components/date-picker/InternalPickers.js | 18 + .../date-picker/InternalPickers.tsx | 18 + .../date-picker/LocalizedDatePicker.js | 61 + .../date-picker/LocalizedDatePicker.tsx | 63 + .../date-picker/ResponsiveDatePickers.js | 46 + .../date-picker/ResponsiveDatePickers.tsx | 46 + .../date-picker/ServerRequestDatePicker.js | 106 + .../date-picker/ServerRequestDatePicker.tsx | 106 + .../date-picker/StaticDatePickerDemo.js | 23 + .../date-picker/StaticDatePickerDemo.tsx | 23 + .../date-picker/StaticDatePickerLandscape.js | 25 + .../date-picker/StaticDatePickerLandscape.tsx | 25 + .../components/date-picker/ViewsDatePicker.js | 74 + .../date-picker/ViewsDatePicker.tsx | 74 + .../components/date-picker/date-picker.md | 105 + .../date-range-picker/BasicDateRangePicker.js | 30 + .../BasicDateRangePicker.tsx | 30 + .../CalendarsDateRangePicker.js | 64 + .../CalendarsDateRangePicker.tsx | 64 + .../CustomDateRangeInputs.js | 24 + .../CustomDateRangeInputs.tsx | 33 + .../MinMaxDateRangePicker.js | 35 + .../MinMaxDateRangePicker.tsx | 35 + .../ResponsiveDateRangePicker.js | 44 + .../ResponsiveDateRangePicker.tsx | 23 +- .../StaticDateRangePicker.js | 29 + .../StaticDateRangePicker.tsx | 31 + .../date-range-picker/date-range-picker.md | 83 + .../date-time-picker/BasicDateTimePicker.js | 20 + .../date-time-picker/BasicDateTimePicker.tsx | 12 +- .../date-time-picker/CustomDateTimePicker.js | 74 + .../date-time-picker/CustomDateTimePicker.tsx | 76 + .../date-time-picker/DateTimeValidation.js | 36 + .../date-time-picker/DateTimeValidation.tsx | 36 + .../ResponsiveDateTimePickers.js | 41 + .../ResponsiveDateTimePickers.tsx | 41 + .../date-time-picker/date-time-picker.md | 71 + .../components/pickers/MaterialUIPickers.js | 27 +- .../components/pickers/MaterialUIPickers.tsx | 27 +- docs/src/pages/components/pickers/pickers.md | 8 +- .../components/progress/DelayingAppearance.js | 4 +- .../progress/DelayingAppearance.tsx | 4 +- .../components/time-picker/BasicTimePicker.js | 33 + .../time-picker/BasicTimePicker.tsx | 33 + .../time-picker/LocalizedTimePicker.js | 56 + .../time-picker/LocalizedTimePicker.tsx | 58 + .../time-picker/ResponsiveTimePickers.js | 41 + .../time-picker/ResponsiveTimePickers.tsx | 41 + .../time-picker/SecondsTimePicker.js | 41 + .../time-picker/SecondsTimePicker.tsx | 41 + .../time-picker/StaticTimePickerDemo.js | 22 + .../time-picker/StaticTimePickerDemo.tsx | 22 + .../time-picker/StaticTimePickerLandscape.js | 24 + .../time-picker/StaticTimePickerLandscape.tsx | 24 + .../time-picker/TimeValidationTimePicker.js | 41 + .../time-picker/TimeValidationTimePicker.tsx | 43 + .../components/time-picker/time-picker.md | 80 + docs/translations/translations.json | 7 +- package.json | 5 +- packages/eslint-plugin-material-ui/README.md | 1 + .../src/v1.0.0/import-path.test/actual.js | 6 +- packages/material-ui-lab/package.json | 31 +- .../src/ClockPicker}/Clock.tsx | 85 +- .../src/ClockPicker}/ClockNumber.tsx | 91 +- .../src/ClockPicker}/ClockNumbers.tsx | 11 +- .../src/ClockPicker/ClockPicker.tsx} | 255 +- .../ClockPickerStandalone.test.tsx | 22 + .../src/ClockPicker/ClockPickerStandalone.tsx | 62 + .../src/ClockPicker}/ClockPointer.tsx | 21 +- .../material-ui-lab/src/ClockPicker/index.ts | 5 + .../src/DatePicker/DatePicker.spec.tsx | 112 + .../src/DatePicker/DatePicker.test.tsx | 521 + .../src/DatePicker/DatePicker.tsx | 298 + .../DatePicker/DatePickerKeyboard.test.tsx | 275 + .../DatePickerLocalization.test.tsx | 127 + .../src/DatePicker/DatePickerToolbar.tsx | 88 + .../material-ui-lab/src/DatePicker/index.ts | 2 + .../DateRangeDelimiter.tsx | 15 +- .../src/DateRangeDelimiter/index.ts | 2 + .../DateRangePicker/DateRangePicker.test.tsx | 289 + .../src/DateRangePicker/DateRangePicker.tsx | 372 + .../DateRangePicker/DateRangePickerInput.tsx | 58 +- .../DateRangePickerToolbar.tsx | 89 + .../DateRangePicker/DateRangePickerView.tsx | 58 +- .../DateRangePickerViewDesktop.tsx | 58 +- .../DateRangePickerViewMobile.tsx | 30 +- .../src/DateRangePicker/RangeTypes.ts | 17 + .../date-range-manager.test.ts | 152 + .../src/DateRangePicker/date-range-manager.ts | 25 +- .../src/DateRangePicker/index.ts | 2 + .../DateRangePicker/makeDateRangePicker.tsx} | 90 +- .../DateRangePickerDay.test.tsx | 56 + .../DateRangePickerDay.tsx | 152 +- .../src/DateRangePickerDay/index.ts | 2 + .../DateTimePicker/DateTimePicker.spec.tsx | 9 + .../DateTimePicker/DateTimePicker.test.tsx | 262 + .../src/DateTimePicker/DateTimePicker.tsx | 570 + .../src/DateTimePicker/DateTimePickerTabs.tsx | 66 +- .../DateTimePicker/DateTimePickerToolbar.tsx | 150 + .../src/DateTimePicker/date-time-utils.ts | 14 +- .../src/DateTimePicker/index.ts | 2 + .../src/DayPicker/DayPicker.test.tsx | 59 + .../src/DayPicker/DayPicker.tsx | 347 + .../src/DayPicker/PickersCalendar.tsx} | 75 +- .../src/DayPicker/PickersCalendarHeader.tsx} | 90 +- .../DayPicker/PickersFadeTransitionGroup.tsx} | 69 +- .../src/DayPicker/PickersSlideTransition.tsx | 120 + .../material-ui-lab/src/DayPicker/index.ts | 4 + .../src/DayPicker}/useCalendarState.tsx | 35 +- .../DesktopDatePicker/DesktopDatePicker.tsx | 216 + .../src/DesktopDatePicker/index.ts | 2 + .../DesktopDateRangePicker.tsx | 335 + .../src/DesktopDateRangePicker/index.ts | 2 + .../DesktopDateTimePicker.tsx | 408 + .../src/DesktopDateTimePicker/index.ts | 2 + .../DesktopTimePicker/DesktopTimePicker.tsx | 256 + .../src/DesktopTimePicker/index.ts | 2 + .../LocalizationProvider.tsx | 99 + .../src/LocalizationProvider/index.ts | 2 + .../src/MobileDatePicker/MobileDatePicker.tsx | 242 + .../src/MobileDatePicker/index.ts | 2 + .../MobileDateRangePicker.tsx | 358 + .../src/MobileDateRangePicker/index.ts | 2 + .../MobileDateTimePicker.tsx | 434 + .../src/MobileDateTimePicker/index.ts | 2 + .../src/MobileTimePicker/MobileTimePicker.tsx | 282 + .../src/MobileTimePicker/index.ts | 2 + .../src/MonthPicker/MonthPicker.test.tsx | 61 + .../src/MonthPicker/MonthPicker.tsx | 151 + .../src/MonthPicker/PickersMonth.tsx} | 29 +- .../material-ui-lab/src/MonthPicker/index.ts | 4 + .../PickersCalendarSkeleton.tsx | 83 + .../src/PickersCalendarSkeleton/index.ts | 3 + .../src/PickersDay/PickersDay.test.tsx | 44 + .../src/PickersDay/PickersDay.tsx} | 208 +- .../material-ui-lab/src/PickersDay/index.ts | 4 + .../src/StaticDatePicker/StaticDatePicker.tsx | 213 + .../src/StaticDatePicker/index.ts | 2 + .../StaticDateRangePicker.tsx | 329 + .../src/StaticDateRangePicker/index.ts | 2 + .../StaticDateTimePicker.tsx | 405 + .../src/StaticDateTimePicker/index.ts | 2 + .../src/StaticTimePicker/StaticTimePicker.tsx | 253 + .../src/StaticTimePicker/index.ts | 2 + .../src/TimePicker/TimePicker.spec.tsx | 20 + .../src/TimePicker/TimePicker.test.tsx | 287 + .../src/TimePicker/TimePicker.tsx | 367 + .../src/TimePicker/TimePickerToolbar.tsx | 168 + .../material-ui-lab/src/TimePicker/index.tsx | 2 + .../src/YearPicker/PickersYear.tsx | 115 + .../src/YearPicker/YearPicker.test.tsx | 64 + .../src/YearPicker/YearPicker.tsx | 239 + .../material-ui-lab/src/YearPicker/index.ts | 4 + .../src/dateAdapter}/date-fns.ts | 0 .../src/dateAdapter}/dayjs.ts | 0 .../src/dateAdapter}/luxon.ts | 0 .../src/dateAdapter}/moment.ts | 0 packages/material-ui-lab/src/index.d.ts | 69 + packages/material-ui-lab/src/index.js | 65 + packages/material-ui-lab/src/index.test.js | 2 +- .../internal/pickers}/KeyboardDateInput.tsx | 8 +- .../src/internal/pickers}/Picker/Picker.tsx | 135 +- .../internal/pickers/Picker/PickerView.tsx | 16 + .../pickers/Picker/SharedPickerProps.tsx | 37 + .../pickers}/Picker/makePickerWithState.tsx | 35 +- .../pickers/PickersArrowSwitcher.tsx} | 37 +- .../internal/pickers}/PickersModalDialog.tsx | 98 +- .../src/internal/pickers}/PickersPopper.tsx | 65 +- .../src/internal/pickers/PickersToolbar.tsx} | 79 +- .../pickers/PickersToolbarButton.tsx} | 43 +- .../internal/pickers/PickersToolbarText.tsx | 47 + .../src/internal/pickers}/PureDateInput.tsx | 32 +- .../src/internal/pickers/ToolbarText.tsx | 45 + .../internal/pickers}/constants/ClockType.ts | 0 .../internal/pickers}/constants/dimensions.ts | 0 .../internal/pickers/constants/prop-types.ts | 5 + .../src/internal/pickers/date-utils.test.ts | 199 + .../src/internal/pickers}/date-utils.ts | 75 +- .../pickers}/hooks/date-helpers-hooks.tsx | 35 +- .../pickers}/hooks/useCanAutoFocus.tsx | 10 +- .../pickers}/hooks/useIsLandscape.tsx | 17 +- .../src/internal/pickers}/hooks/useKeyDown.ts | 9 +- .../pickers}/hooks/useMaskedInput.tsx | 8 +- .../internal/pickers}/hooks/useOpenState.ts | 14 +- .../internal/pickers}/hooks/usePickerState.ts | 28 +- .../src/internal/pickers}/hooks/useUtils.ts | 10 +- .../internal/pickers}/hooks/useValidation.ts | 4 +- .../src/internal/pickers/hooks/useViews.tsx | 78 + .../src/internal/pickers/test-utils.tsx | 78 + .../pickers/text-field-helper.test.ts | 74 + .../internal/pickers}/text-field-helper.ts | 17 +- .../src/internal/pickers}/time-utils.ts | 13 +- .../internal/pickers}/typings/BasePicker.tsx | 50 +- .../src/internal/pickers/typings/Views.ts | 7 + .../src/internal/pickers}/typings/helpers.ts | 4 + .../src/internal/pickers}/utils.ts | 14 +- .../internal/pickers}/withDateAdapterProp.tsx | 4 +- .../internal/pickers}/withDefaultProps.tsx | 2 +- .../wrappers/DesktopTooltipWrapper.tsx | 28 +- .../pickers}/wrappers/DesktopWrapper.tsx | 24 +- .../pickers}/wrappers/MobileWrapper.tsx | 38 +- .../pickers}/wrappers/ResponsiveWrapper.tsx | 25 +- .../pickers}/wrappers/StaticWrapper.tsx | 30 +- .../src/internal/pickers/wrappers/Wrapper.tsx | 41 + .../internal/pickers/wrappers/WrapperProps.ts | 41 + .../wrappers/WrapperVariantContext.tsx | 14 + .../wrappers/makeWrapperComponent.tsx | 41 +- .../src/internal/svg-icons/ArrowDropDown.js | 7 + .../src/internal/svg-icons}/ArrowDropDown.tsx | 2 +- .../src/internal/svg-icons}/ArrowLeft.tsx | 4 +- .../src/internal/svg-icons}/ArrowRight.tsx | 4 +- .../src/internal/svg-icons/Calendar.tsx} | 4 +- .../src/internal/svg-icons}/Clock.tsx | 4 +- .../src/internal/svg-icons}/DateRange.tsx | 4 +- .../src/internal/svg-icons}/Pen.tsx | 4 +- .../src/internal/svg-icons}/Time.tsx | 4 +- .../src/themeAugmentation/overrides.d.ts | 51 + .../src/themeAugmentation/props.d.ts | 37 +- packages/material-ui-lab/tsconfig.json | 7 +- packages/material-ui/src/Rating/Rating.d.ts | 4 - packages/material-ui/src/Rating/Rating.js | 4 - packages/material-ui/tsconfig.build.json | 1 + packages/pickers/.circleci/config.yml | 175 - packages/pickers/.dependabot/config.yml | 43 - packages/pickers/.editorconfig | 9 - packages/pickers/.eslintrc.js | 197 - packages/pickers/.gitattributes | 3 - packages/pickers/.github/FUNDING.yml | 8 - .../pickers/.github/ISSUE_TEMPLATE/1.bug.md | 66 - .../.github/ISSUE_TEMPLATE/2.feature.md | 34 - .../ISSUE_TEMPLATE/3.material-design.md | 5 - .../pickers/.github/ISSUE_TEMPLATE/config.yml | 5 - .../pickers/.github/PULL_REQUEST_TEMPLATE.md | 3 - packages/pickers/.gitignore | 40 - packages/pickers/.npmignore | 4 - packages/pickers/.percy.yml | 6 - packages/pickers/.prettierignore | 7 - packages/pickers/.prettierrc | 6 - packages/pickers/.vscode/launch.json | 39 - packages/pickers/.vscode/settings.json | 14 - packages/pickers/.vscode/tasks.json | 13 - packages/pickers/CHANGELOG.md | 35 - packages/pickers/CONTRIBUTING.md | 76 - packages/pickers/LICENSE | 21 - packages/pickers/README.md | 52 - packages/pickers/cypress.json | 10 - .../pickers/cypress/fixtures/example.json | 5 - packages/pickers/docs/.gitignore | 1 - packages/pickers/docs/_constants.ts | 11 - packages/pickers/docs/_shared/Ad.tsx | 81 - packages/pickers/docs/_shared/Code.tsx | 71 - packages/pickers/docs/_shared/Example.tsx | 138 - .../pickers/docs/_shared/LinkedComponents.tsx | 25 - packages/pickers/docs/_shared/PageMeta.tsx | 39 - .../pickers/docs/_shared/PatreonSponsors.jsx | 68 - .../pickers/docs/_shared/PropTypesTable.tsx | 151 - .../docs/_shared/UtilsServiceContext.tsx | 17 - .../docs/_shared/svgIcons/GithubIcon.jsx | 10 - .../docs/_shared/svgIcons/KawaiiIcon.tsx | 50 - .../docs/_shared/svgIcons/LightbulbIcon.tsx | 14 - .../pickers/docs/_shared/svgIcons/Logo.tsx | 57 - packages/pickers/docs/babel.config.js | 16 - packages/pickers/docs/fakeApi/randomDate.ts | 33 - packages/pickers/docs/layout/Layout.tsx | 221 - .../pickers/docs/layout/PageWithContext.tsx | 128 - .../docs/layout/components/DrawerMenu.tsx | 49 - .../docs/layout/components/NavItem.jsx | 110 - .../docs/layout/components/NavigationMenu.tsx | 38 - .../docs/layout/components/navigationMap.ts | 56 - .../pickers/docs/layout/styleOverrides.ts | 99 - .../pickers/docs/loaders/example-loader.js | 17 - packages/pickers/docs/next.config.js | 82 - packages/pickers/docs/notifications.json | 1 - packages/pickers/docs/package.json | 89 - packages/pickers/docs/pages/_app.tsx | 46 - packages/pickers/docs/pages/_document.tsx | 112 - packages/pickers/docs/pages/_error.tsx | 42 - packages/pickers/docs/pages/api/props.tsx | 67 - .../datepicker/BasicDatePicker.example.tsx | 16 - .../demo/datepicker/CustomInput.example.tsx | 26 - .../demo/datepicker/DatePickers.example.tsx | 41 - .../demo/datepicker/ServerRequest.example.tsx | 67 - .../datepicker/StaticDatePicker.example.tsx | 35 - .../datepicker/ViewsDatePicker.example.tsx | 36 - .../docs/pages/demo/datepicker/index.mdx | 85 - .../BasicDateRangePicker.example.tsx | 23 - .../CalendarsDateRangePicker.example.tsx | 53 - .../CustomRangeInputs.example.tsx | 23 - .../MinMaxDateRangePicker.example.tsx | 35 - .../StaticDateRangePicker.example.tsx | 36 - .../docs/pages/demo/daterangepicker/index.mdx | 74 - .../CustomDateTimePicker.example.tsx | 56 - .../DateTimePickers.example.tsx | 41 - .../DateTimeValidation.example.tsx | 28 - .../docs/pages/demo/datetime-picker/index.mdx | 56 - .../timepicker/BasicTimePicker.example.tsx | 25 - .../timepicker/SecondsTimePicker.example.tsx | 34 - .../timepicker/StaticTimePicker.example.tsx | 26 - .../demo/timepicker/TimePickers.example.tsx | 39 - .../timepicker/TimeValidation.example.tsx | 42 - .../docs/pages/demo/timepicker/index.mdx | 61 - .../pages/getting-started/installation.mdx | 57 - .../docs/pages/getting-started/parsing.mdx | 20 - .../docs/pages/getting-started/usage.mdx | 61 - .../pages/guides/Accessibility.example.tsx | 16 - .../ControllingProgrammatically.example.tsx | 34 - .../pages/guides/CssOverrides.example.tsx | 58 - .../docs/pages/guides/CssTheme.example.tsx | 26 - .../pages/guides/CssThemeSpacing.example.tsx | 23 - .../pages/guides/DateAdapterProp.example.tsx | 36 - .../docs/pages/guides/Formats.example.tsx | 27 - .../guides/FormikOurValidation.example.tsx | 115 - .../guides/FormikValidationSchema.example.tsx | 71 - .../pages/guides/OverrideLogic.example.tsx | 25 - .../pages/guides/StaticComponents.example.tsx | 38 - .../docs/pages/guides/accessibility.mdx | 68 - .../guides/controlling-programmatically.mdx | 14 - .../docs/pages/guides/css-overrides.mdx | 54 - .../pages/guides/date-adapter-passing.mdx | 26 - .../pages/guides/date-io-customization.mdx | 38 - packages/pickers/docs/pages/guides/forms.mdx | 44 - .../docs/pages/guides/static-components.mdx | 36 - .../pickers/docs/pages/guides/typescript.mdx | 83 - .../docs/pages/guides/upgrading-to-v3.mdx | 138 - packages/pickers/docs/pages/index/Landing.tsx | 190 - .../docs/pages/index/LandingProperty.tsx | 46 - packages/pickers/docs/pages/index/index.tsx | 1 - .../pages/localization/Date-fns.example.tsx | 48 - .../docs/pages/localization/Hijri.example.tsx | 47 - .../pages/localization/Moment.example.tsx | 57 - .../pages/localization/Persian.example.tsx | 47 - .../pages/localization/calendar-systems.mdx | 63 - .../docs/pages/localization/date-fns.mdx | 18 - .../docs/pages/localization/moment.mdx | 22 - .../docs/pages/regression/Regression.tsx | 103 - .../docs/pages/regression/RegressionDay.tsx | 11 - .../pickers/docs/pages/regression/index.tsx | 1 - packages/pickers/docs/pages/releases.tsx | 121 - packages/pickers/docs/patrons.json | 95 - packages/pickers/docs/prop-types.json | 4485 ----- .../docs/scripts/assign-production-domains.js | 9 - packages/pickers/docs/scripts/docgen.js | 101 - .../pickers/docs/scripts/generate-backers.js | 42 - .../docs/static/android-icon-144x144.png | Bin 10632 -> 0 bytes .../docs/static/android-icon-192x192.png | Bin 14815 -> 0 bytes .../docs/static/android-icon-36x36.png | Bin 2735 -> 0 bytes .../docs/static/android-icon-48x48.png | Bin 3499 -> 0 bytes .../docs/static/android-icon-72x72.png | Bin 5070 -> 0 bytes .../docs/static/android-icon-96x96.png | Bin 6655 -> 0 bytes .../docs/static/apple-icon-114x114.png | Bin 7800 -> 0 bytes .../docs/static/apple-icon-120x120.png | Bin 8404 -> 0 bytes .../docs/static/apple-icon-144x144.png | Bin 10632 -> 0 bytes .../docs/static/apple-icon-152x152.png | Bin 11317 -> 0 bytes .../docs/static/apple-icon-180x180.png | Bin 14578 -> 0 bytes .../pickers/docs/static/apple-icon-57x57.png | Bin 4122 -> 0 bytes .../pickers/docs/static/apple-icon-60x60.png | Bin 4357 -> 0 bytes .../pickers/docs/static/apple-icon-72x72.png | Bin 5070 -> 0 bytes .../pickers/docs/static/apple-icon-76x76.png | Bin 5274 -> 0 bytes .../docs/static/apple-icon-precomposed.png | Bin 15304 -> 0 bytes packages/pickers/docs/static/apple-icon.png | Bin 15304 -> 0 bytes .../pickers/docs/static/favicon-16x16.png | Bin 1398 -> 0 bytes .../pickers/docs/static/favicon-32x32.png | Bin 2416 -> 0 bytes .../pickers/docs/static/favicon-96x96.png | Bin 6655 -> 0 bytes packages/pickers/docs/static/favicon.ico | Bin 1150 -> 0 bytes packages/pickers/docs/static/manifest.json | 13 - packages/pickers/docs/static/meta-image.png | Bin 22068 -> 0 bytes .../pickers/docs/static/ms-icon-144x144.png | Bin 10632 -> 0 bytes .../pickers/docs/static/ms-icon-150x150.png | Bin 11174 -> 0 bytes .../pickers/docs/static/ms-icon-310x310.png | Bin 32197 -> 0 bytes .../pickers/docs/static/ms-icon-70x70.png | Bin 4886 -> 0 bytes packages/pickers/docs/tsconfig.json | 27 - packages/pickers/docs/typings.d.ts | 30 - .../docs/utils/NotificationManager.tsx | 63 - .../pickers/docs/utils/anchor-autolink.js | 35 - packages/pickers/docs/utils/getPageContext.ts | 19 - packages/pickers/docs/utils/github-api.ts | 36 - packages/pickers/docs/utils/helpers.ts | 46 - packages/pickers/docs/utils/prism.ts | 32 - packages/pickers/docs/utils/table-styler.js | 7 - packages/pickers/docs/utils/utilsService.ts | 88 - .../pickers/e2e/component/DatePicker.spec.tsx | 24 - .../e2e/component/DateTimePicker.spec.tsx | 48 - .../e2e/component/KeyboardNavigation.spec.tsx | 97 - packages/pickers/e2e/integration/App.spec.ts | 28 - .../e2e/integration/DatePicker.spec.ts | 108 - .../pickers/e2e/integration/DateRange.spec.ts | 94 - .../e2e/integration/VisualRegression.spec.ts | 136 - packages/pickers/e2e/plugins/index.js | 28 - packages/pickers/e2e/support/commands.ts | 17 - packages/pickers/e2e/support/index.js | 20 - packages/pickers/e2e/test-utils.tsx | 66 - packages/pickers/e2e/tsconfig.json | 21 - packages/pickers/lib/.size-snapshot.json | 26 - packages/pickers/lib/babel.config.js | 27 - packages/pickers/lib/cypress.json | 3 - packages/pickers/lib/jest.config.js | 19 - packages/pickers/lib/package.json | 142 - packages/pickers/lib/prepare-build-files.js | 71 - packages/pickers/lib/remove-prop-types.js | 37 - packages/pickers/lib/rollup.config.js | 195 - packages/pickers/lib/src/CalendarSkeleton.tsx | 67 - .../pickers/lib/src/DatePicker/DatePicker.ts | 95 - .../lib/src/DatePicker/DatePickerToolbar.tsx | 91 - packages/pickers/lib/src/DatePicker/index.ts | 1 - .../DateRangePickerToolbar.tsx | 90 - .../lib/src/DateRangePicker/RangeTypes.ts | 17 - .../lib/src/DateTimePicker/DateTimePicker.tsx | 159 - .../DateTimePicker/DateTimePickerToolbar.tsx | 149 - .../pickers/lib/src/DateTimePicker/index.ts | 1 - .../pickers/lib/src/LocalizationProvider.tsx | 37 - .../lib/src/Picker/SharedPickerProps.tsx | 71 - .../pickers/lib/src/TimePicker/TimePicker.tsx | 105 - .../lib/src/TimePicker/TimePickerToolbar.tsx | 191 - packages/pickers/lib/src/TimePicker/index.tsx | 1 - .../lib/src/__tests__/DatePicker.test.tsx | 178 - .../src/__tests__/DatePickerProps.test.tsx | 136 - .../lib/src/__tests__/DatePickerRoot.test.tsx | 137 - .../__tests__/DatePickerTestingLib.test.tsx | 183 - .../src/__tests__/DateRangePicker.test.tsx | 69 - .../__tests__/DateRangePickerLegacy.test.tsx | 48 - .../lib/src/__tests__/DateTimePicker.test.tsx | 111 - .../src/__tests__/DateTimePickerRoot.test.tsx | 61 - .../DateTimePickerTestingLib.test.tsx | 121 - .../src/__tests__/KeyboardDatePicker.test.tsx | 49 - .../__tests__/KeyboardDateTimePicker.test.tsx | 45 - .../pickers/lib/src/__tests__/Theme.test.tsx | 42 - .../lib/src/__tests__/TimePicker.test.tsx | 255 - .../lib/src/__tests__/Validation.test.tsx | 172 - .../pickers/lib/src/__tests__/commands.tsx | 8 - .../lib/src/__tests__/createClientRender.tsx | 182 - packages/pickers/lib/src/__tests__/setup.ts | 42 - .../lib/src/__tests__/shallow/Month.test.tsx | 36 - .../__tests__/shallow/MonthSelection.test.tsx | 37 - .../pickers/lib/src/__tests__/test-utils.tsx | 139 - .../pickers/lib/src/__tests__/tsconfig.json | 11 - .../typescript/GenericValues.spec.tsx | 153 - .../__tests__/typescript/Overrides.tstest.tsx | 54 - .../typescript/ThemeDefaultProps.tstest.tsx | 46 - .../pickers/lib/src/__tests__/typings.d.ts | 9 - .../__tests__/unit/date-range-manager.test.ts | 61 - .../lib/src/__tests__/unit/date-utils.test.ts | 179 - .../__tests__/unit/text-field-helper.test.ts | 74 - .../pickers/lib/src/_shared/ToolbarText.tsx | 48 - .../lib/src/_shared/hooks/useTraceUpdate.tsx | 20 - .../lib/src/_shared/hooks/useViews.tsx | 72 - .../pickers/lib/src/constants/prop-types.ts | 18 - packages/pickers/lib/src/index.ts | 53 - packages/pickers/lib/src/typings/index.ts | 2 - packages/pickers/lib/src/typings/overrides.ts | 68 - packages/pickers/lib/src/typings/props.ts | 60 - .../lib/src/views/Calendar/CalendarView.tsx | 199 - .../lib/src/views/Calendar/MonthSelection.tsx | 90 - .../src/views/Calendar/SlideTransition.tsx | 113 - .../pickers/lib/src/views/Calendar/Year.tsx | 113 - .../lib/src/views/Calendar/YearSelection.tsx | 162 - .../lib/src/views/MobileKeyboardInputView.tsx | 8 - packages/pickers/lib/src/wrappers/Wrapper.tsx | 66 - .../src/wrappers/WrapperVariantContext.tsx | 7 - packages/pickers/lib/tsconfig.adapters.json | 8 - packages/pickers/lib/tsconfig.json | 24 - packages/pickers/lib/typings.d.ts | 3 - packages/pickers/now.json | 23 - packages/pickers/package.json | 82 - packages/pickers/scripts/deduplicate.js | 41 - packages/pickers/yarn.lock | 13529 ---------------- .../typescript-to-proptypes/src/generator.ts | 24 +- .../typescript-to-proptypes/src/injector.ts | 54 +- .../typescript-to-proptypes/src/parser.ts | 49 +- scripts/generateProptypes.ts | 58 +- test/regressions/index.js | 17 +- test/utils/createClientRender.js | 42 +- test/utils/initMatchers.ts | 13 + yarn.lock | 40 +- 491 files changed, 14901 insertions(+), 31833 deletions(-) create mode 100644 docs/pages/components/date-picker.js create mode 100644 docs/pages/components/date-range-picker.js create mode 100644 docs/pages/components/date-time-picker.js create mode 100644 docs/pages/components/time-picker.js create mode 100644 docs/src/pages/components/date-picker/BasicDatePicker.js create mode 100644 docs/src/pages/components/date-picker/BasicDatePicker.tsx create mode 100644 docs/src/pages/components/date-picker/CustomDay.js rename packages/pickers/docs/pages/demo/datepicker/CustomDay.example.tsx => docs/src/pages/components/date-picker/CustomDay.tsx (50%) create mode 100644 docs/src/pages/components/date-picker/CustomInput.js create mode 100644 docs/src/pages/components/date-picker/CustomInput.tsx create mode 100644 docs/src/pages/components/date-picker/InternalPickers.js create mode 100644 docs/src/pages/components/date-picker/InternalPickers.tsx create mode 100644 docs/src/pages/components/date-picker/LocalizedDatePicker.js create mode 100644 docs/src/pages/components/date-picker/LocalizedDatePicker.tsx create mode 100644 docs/src/pages/components/date-picker/ResponsiveDatePickers.js create mode 100644 docs/src/pages/components/date-picker/ResponsiveDatePickers.tsx create mode 100644 docs/src/pages/components/date-picker/ServerRequestDatePicker.js create mode 100644 docs/src/pages/components/date-picker/ServerRequestDatePicker.tsx create mode 100644 docs/src/pages/components/date-picker/StaticDatePickerDemo.js create mode 100644 docs/src/pages/components/date-picker/StaticDatePickerDemo.tsx create mode 100644 docs/src/pages/components/date-picker/StaticDatePickerLandscape.js create mode 100644 docs/src/pages/components/date-picker/StaticDatePickerLandscape.tsx create mode 100644 docs/src/pages/components/date-picker/ViewsDatePicker.js create mode 100644 docs/src/pages/components/date-picker/ViewsDatePicker.tsx create mode 100644 docs/src/pages/components/date-picker/date-picker.md create mode 100644 docs/src/pages/components/date-range-picker/BasicDateRangePicker.js create mode 100644 docs/src/pages/components/date-range-picker/BasicDateRangePicker.tsx create mode 100644 docs/src/pages/components/date-range-picker/CalendarsDateRangePicker.js create mode 100644 docs/src/pages/components/date-range-picker/CalendarsDateRangePicker.tsx create mode 100644 docs/src/pages/components/date-range-picker/CustomDateRangeInputs.js create mode 100644 docs/src/pages/components/date-range-picker/CustomDateRangeInputs.tsx create mode 100644 docs/src/pages/components/date-range-picker/MinMaxDateRangePicker.js create mode 100644 docs/src/pages/components/date-range-picker/MinMaxDateRangePicker.tsx create mode 100644 docs/src/pages/components/date-range-picker/ResponsiveDateRangePicker.js rename packages/pickers/docs/pages/demo/daterangepicker/ResponsiveDateRangePicker.example.tsx => docs/src/pages/components/date-range-picker/ResponsiveDateRangePicker.tsx (60%) create mode 100644 docs/src/pages/components/date-range-picker/StaticDateRangePicker.js create mode 100644 docs/src/pages/components/date-range-picker/StaticDateRangePicker.tsx create mode 100644 docs/src/pages/components/date-range-picker/date-range-picker.md create mode 100644 docs/src/pages/components/date-time-picker/BasicDateTimePicker.js rename packages/pickers/docs/pages/demo/datetime-picker/BasicDateTimePicker.example.tsx => docs/src/pages/components/date-time-picker/BasicDateTimePicker.tsx (57%) create mode 100644 docs/src/pages/components/date-time-picker/CustomDateTimePicker.js create mode 100644 docs/src/pages/components/date-time-picker/CustomDateTimePicker.tsx create mode 100644 docs/src/pages/components/date-time-picker/DateTimeValidation.js create mode 100644 docs/src/pages/components/date-time-picker/DateTimeValidation.tsx create mode 100644 docs/src/pages/components/date-time-picker/ResponsiveDateTimePickers.js create mode 100644 docs/src/pages/components/date-time-picker/ResponsiveDateTimePickers.tsx create mode 100644 docs/src/pages/components/date-time-picker/date-time-picker.md create mode 100644 docs/src/pages/components/time-picker/BasicTimePicker.js create mode 100644 docs/src/pages/components/time-picker/BasicTimePicker.tsx create mode 100644 docs/src/pages/components/time-picker/LocalizedTimePicker.js create mode 100644 docs/src/pages/components/time-picker/LocalizedTimePicker.tsx create mode 100644 docs/src/pages/components/time-picker/ResponsiveTimePickers.js create mode 100644 docs/src/pages/components/time-picker/ResponsiveTimePickers.tsx create mode 100644 docs/src/pages/components/time-picker/SecondsTimePicker.js create mode 100644 docs/src/pages/components/time-picker/SecondsTimePicker.tsx create mode 100644 docs/src/pages/components/time-picker/StaticTimePickerDemo.js create mode 100644 docs/src/pages/components/time-picker/StaticTimePickerDemo.tsx create mode 100644 docs/src/pages/components/time-picker/StaticTimePickerLandscape.js create mode 100644 docs/src/pages/components/time-picker/StaticTimePickerLandscape.tsx create mode 100644 docs/src/pages/components/time-picker/TimeValidationTimePicker.js create mode 100644 docs/src/pages/components/time-picker/TimeValidationTimePicker.tsx create mode 100644 docs/src/pages/components/time-picker/time-picker.md rename packages/{pickers/lib/src/views/Clock => material-ui-lab/src/ClockPicker}/Clock.tsx (73%) rename packages/{pickers/lib/src/views/Clock => material-ui-lab/src/ClockPicker}/ClockNumber.tsx (55%) rename packages/{pickers/lib/src/views/Clock => material-ui-lab/src/ClockPicker}/ClockNumbers.tsx (90%) rename packages/{pickers/lib/src/views/Clock/ClockView.tsx => material-ui-lab/src/ClockPicker/ClockPicker.tsx} (56%) create mode 100644 packages/material-ui-lab/src/ClockPicker/ClockPickerStandalone.test.tsx create mode 100644 packages/material-ui-lab/src/ClockPicker/ClockPickerStandalone.tsx rename packages/{pickers/lib/src/views/Clock => material-ui-lab/src/ClockPicker}/ClockPointer.tsx (86%) create mode 100644 packages/material-ui-lab/src/ClockPicker/index.ts create mode 100644 packages/material-ui-lab/src/DatePicker/DatePicker.spec.tsx create mode 100644 packages/material-ui-lab/src/DatePicker/DatePicker.test.tsx create mode 100644 packages/material-ui-lab/src/DatePicker/DatePicker.tsx create mode 100644 packages/material-ui-lab/src/DatePicker/DatePickerKeyboard.test.tsx create mode 100644 packages/material-ui-lab/src/DatePicker/DatePickerLocalization.test.tsx create mode 100644 packages/material-ui-lab/src/DatePicker/DatePickerToolbar.tsx create mode 100644 packages/material-ui-lab/src/DatePicker/index.ts rename packages/{pickers/lib/src/DateRangePicker => material-ui-lab/src/DateRangeDelimiter}/DateRangeDelimiter.tsx (63%) create mode 100644 packages/material-ui-lab/src/DateRangeDelimiter/index.ts create mode 100644 packages/material-ui-lab/src/DateRangePicker/DateRangePicker.test.tsx create mode 100644 packages/material-ui-lab/src/DateRangePicker/DateRangePicker.tsx rename packages/{pickers/lib => material-ui-lab}/src/DateRangePicker/DateRangePickerInput.tsx (78%) create mode 100644 packages/material-ui-lab/src/DateRangePicker/DateRangePickerToolbar.tsx rename packages/{pickers/lib => material-ui-lab}/src/DateRangePicker/DateRangePickerView.tsx (81%) rename packages/{pickers/lib => material-ui-lab}/src/DateRangePicker/DateRangePickerViewDesktop.tsx (78%) rename packages/{pickers/lib => material-ui-lab}/src/DateRangePicker/DateRangePickerViewMobile.tsx (73%) create mode 100644 packages/material-ui-lab/src/DateRangePicker/RangeTypes.ts create mode 100644 packages/material-ui-lab/src/DateRangePicker/date-range-manager.test.ts rename packages/{pickers/lib => material-ui-lab}/src/DateRangePicker/date-range-manager.ts (59%) create mode 100644 packages/material-ui-lab/src/DateRangePicker/index.ts rename packages/{pickers/lib/src/DateRangePicker/DateRangePicker.tsx => material-ui-lab/src/DateRangePicker/makeDateRangePicker.tsx} (55%) create mode 100644 packages/material-ui-lab/src/DateRangePickerDay/DateRangePickerDay.test.tsx rename packages/{pickers/lib/src/DateRangePicker => material-ui-lab/src/DateRangePickerDay}/DateRangePickerDay.tsx (50%) create mode 100644 packages/material-ui-lab/src/DateRangePickerDay/index.ts create mode 100644 packages/material-ui-lab/src/DateTimePicker/DateTimePicker.spec.tsx create mode 100644 packages/material-ui-lab/src/DateTimePicker/DateTimePicker.test.tsx create mode 100644 packages/material-ui-lab/src/DateTimePicker/DateTimePicker.tsx rename packages/{pickers/lib => material-ui-lab}/src/DateTimePicker/DateTimePickerTabs.tsx (55%) create mode 100644 packages/material-ui-lab/src/DateTimePicker/DateTimePickerToolbar.tsx rename packages/{pickers/lib => material-ui-lab}/src/DateTimePicker/date-time-utils.ts (54%) create mode 100644 packages/material-ui-lab/src/DateTimePicker/index.ts create mode 100644 packages/material-ui-lab/src/DayPicker/DayPicker.test.tsx create mode 100644 packages/material-ui-lab/src/DayPicker/DayPicker.tsx rename packages/{pickers/lib/src/views/Calendar/Calendar.tsx => material-ui-lab/src/DayPicker/PickersCalendar.tsx} (77%) rename packages/{pickers/lib/src/views/Calendar/CalendarHeader.tsx => material-ui-lab/src/DayPicker/PickersCalendarHeader.tsx} (75%) rename packages/{pickers/lib/src/views/Calendar/FadeTransitionGroup.tsx => material-ui-lab/src/DayPicker/PickersFadeTransitionGroup.tsx} (53%) create mode 100644 packages/material-ui-lab/src/DayPicker/PickersSlideTransition.tsx create mode 100644 packages/material-ui-lab/src/DayPicker/index.ts rename packages/{pickers/lib/src/views/Calendar => material-ui-lab/src/DayPicker}/useCalendarState.tsx (84%) create mode 100644 packages/material-ui-lab/src/DesktopDatePicker/DesktopDatePicker.tsx create mode 100644 packages/material-ui-lab/src/DesktopDatePicker/index.ts create mode 100644 packages/material-ui-lab/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx create mode 100644 packages/material-ui-lab/src/DesktopDateRangePicker/index.ts create mode 100644 packages/material-ui-lab/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx create mode 100644 packages/material-ui-lab/src/DesktopDateTimePicker/index.ts create mode 100644 packages/material-ui-lab/src/DesktopTimePicker/DesktopTimePicker.tsx create mode 100644 packages/material-ui-lab/src/DesktopTimePicker/index.ts create mode 100644 packages/material-ui-lab/src/LocalizationProvider/LocalizationProvider.tsx create mode 100644 packages/material-ui-lab/src/LocalizationProvider/index.ts create mode 100644 packages/material-ui-lab/src/MobileDatePicker/MobileDatePicker.tsx create mode 100644 packages/material-ui-lab/src/MobileDatePicker/index.ts create mode 100644 packages/material-ui-lab/src/MobileDateRangePicker/MobileDateRangePicker.tsx create mode 100644 packages/material-ui-lab/src/MobileDateRangePicker/index.ts create mode 100644 packages/material-ui-lab/src/MobileDateTimePicker/MobileDateTimePicker.tsx create mode 100644 packages/material-ui-lab/src/MobileDateTimePicker/index.ts create mode 100644 packages/material-ui-lab/src/MobileTimePicker/MobileTimePicker.tsx create mode 100644 packages/material-ui-lab/src/MobileTimePicker/index.ts create mode 100644 packages/material-ui-lab/src/MonthPicker/MonthPicker.test.tsx create mode 100644 packages/material-ui-lab/src/MonthPicker/MonthPicker.tsx rename packages/{pickers/lib/src/views/Calendar/Month.tsx => material-ui-lab/src/MonthPicker/PickersMonth.tsx} (69%) create mode 100644 packages/material-ui-lab/src/MonthPicker/index.ts create mode 100644 packages/material-ui-lab/src/PickersCalendarSkeleton/PickersCalendarSkeleton.tsx create mode 100644 packages/material-ui-lab/src/PickersCalendarSkeleton/index.ts create mode 100644 packages/material-ui-lab/src/PickersDay/PickersDay.test.tsx rename packages/{pickers/lib/src/views/Calendar/Day.tsx => material-ui-lab/src/PickersDay/PickersDay.tsx} (52%) create mode 100644 packages/material-ui-lab/src/PickersDay/index.ts create mode 100644 packages/material-ui-lab/src/StaticDatePicker/StaticDatePicker.tsx create mode 100644 packages/material-ui-lab/src/StaticDatePicker/index.ts create mode 100644 packages/material-ui-lab/src/StaticDateRangePicker/StaticDateRangePicker.tsx create mode 100644 packages/material-ui-lab/src/StaticDateRangePicker/index.ts create mode 100644 packages/material-ui-lab/src/StaticDateTimePicker/StaticDateTimePicker.tsx create mode 100644 packages/material-ui-lab/src/StaticDateTimePicker/index.ts create mode 100644 packages/material-ui-lab/src/StaticTimePicker/StaticTimePicker.tsx create mode 100644 packages/material-ui-lab/src/StaticTimePicker/index.ts create mode 100644 packages/material-ui-lab/src/TimePicker/TimePicker.spec.tsx create mode 100644 packages/material-ui-lab/src/TimePicker/TimePicker.test.tsx create mode 100644 packages/material-ui-lab/src/TimePicker/TimePicker.tsx create mode 100644 packages/material-ui-lab/src/TimePicker/TimePickerToolbar.tsx create mode 100644 packages/material-ui-lab/src/TimePicker/index.tsx create mode 100644 packages/material-ui-lab/src/YearPicker/PickersYear.tsx create mode 100644 packages/material-ui-lab/src/YearPicker/YearPicker.test.tsx create mode 100644 packages/material-ui-lab/src/YearPicker/YearPicker.tsx create mode 100644 packages/material-ui-lab/src/YearPicker/index.ts rename packages/{pickers/lib/adapter => material-ui-lab/src/dateAdapter}/date-fns.ts (100%) rename packages/{pickers/lib/adapter => material-ui-lab/src/dateAdapter}/dayjs.ts (100%) rename packages/{pickers/lib/adapter => material-ui-lab/src/dateAdapter}/luxon.ts (100%) rename packages/{pickers/lib/adapter => material-ui-lab/src/dateAdapter}/moment.ts (100%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/KeyboardDateInput.tsx (91%) rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/Picker/Picker.tsx (55%) create mode 100644 packages/material-ui-lab/src/internal/pickers/Picker/PickerView.tsx create mode 100644 packages/material-ui-lab/src/internal/pickers/Picker/SharedPickerProps.tsx rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/Picker/makePickerWithState.tsx (77%) rename packages/{pickers/lib/src/_shared/ArrowSwitcher.tsx => material-ui-lab/src/internal/pickers/PickersArrowSwitcher.tsx} (78%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/PickersModalDialog.tsx (63%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/PickersPopper.tsx (62%) rename packages/{pickers/lib/src/_shared/PickerToolbar.tsx => material-ui-lab/src/internal/pickers/PickersToolbar.tsx} (62%) rename packages/{pickers/lib/src/_shared/ToolbarButton.tsx => material-ui-lab/src/internal/pickers/PickersToolbarButton.tsx} (52%) create mode 100644 packages/material-ui-lab/src/internal/pickers/PickersToolbarText.tsx rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/PureDateInput.tsx (84%) create mode 100644 packages/material-ui-lab/src/internal/pickers/ToolbarText.tsx rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/constants/ClockType.ts (100%) rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/constants/dimensions.ts (100%) create mode 100644 packages/material-ui-lab/src/internal/pickers/constants/prop-types.ts create mode 100644 packages/material-ui-lab/src/internal/pickers/date-utils.test.ts rename packages/{pickers/lib/src/_helpers => material-ui-lab/src/internal/pickers}/date-utils.ts (78%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/date-helpers-hooks.tsx (64%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/useCanAutoFocus.tsx (60%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/useIsLandscape.tsx (77%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/useKeyDown.ts (86%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/useMaskedInput.tsx (95%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/useOpenState.ts (78%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/usePickerState.ts (85%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/useUtils.ts (67%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/hooks/useValidation.ts (95%) create mode 100644 packages/material-ui-lab/src/internal/pickers/hooks/useViews.tsx create mode 100644 packages/material-ui-lab/src/internal/pickers/test-utils.tsx create mode 100644 packages/material-ui-lab/src/internal/pickers/text-field-helper.test.ts rename packages/{pickers/lib/src/_helpers => material-ui-lab/src/internal/pickers}/text-field-helper.ts (86%) rename packages/{pickers/lib/src/_helpers => material-ui-lab/src/internal/pickers}/time-utils.ts (95%) rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/typings/BasePicker.tsx (50%) create mode 100644 packages/material-ui-lab/src/internal/pickers/typings/Views.ts rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/typings/helpers.ts (77%) rename packages/{pickers/lib/src/_helpers => material-ui-lab/src/internal/pickers}/utils.ts (76%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/withDateAdapterProp.tsx (88%) rename packages/{pickers/lib/src/_shared => material-ui-lab/src/internal/pickers}/withDefaultProps.tsx (95%) rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/wrappers/DesktopTooltipWrapper.tsx (57%) rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/wrappers/DesktopWrapper.tsx (55%) rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/wrappers/MobileWrapper.tsx (54%) rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/wrappers/ResponsiveWrapper.tsx (70%) rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/wrappers/StaticWrapper.tsx (54%) create mode 100644 packages/material-ui-lab/src/internal/pickers/wrappers/Wrapper.tsx create mode 100644 packages/material-ui-lab/src/internal/pickers/wrappers/WrapperProps.ts create mode 100644 packages/material-ui-lab/src/internal/pickers/wrappers/WrapperVariantContext.tsx rename packages/{pickers/lib/src => material-ui-lab/src/internal/pickers}/wrappers/makeWrapperComponent.tsx (60%) create mode 100644 packages/material-ui-lab/src/internal/svg-icons/ArrowDropDown.js rename packages/{pickers/lib/src/_shared/icons => material-ui-lab/src/internal/svg-icons}/ArrowDropDown.tsx (58%) rename packages/{pickers/lib/src/_shared/icons => material-ui-lab/src/internal/svg-icons}/ArrowLeft.tsx (78%) rename packages/{pickers/lib/src/_shared/icons => material-ui-lab/src/internal/svg-icons}/ArrowRight.tsx (77%) rename packages/{pickers/lib/src/_shared/icons/CalendarIcon.tsx => material-ui-lab/src/internal/svg-icons/Calendar.tsx} (83%) rename packages/{pickers/lib/src/_shared/icons => material-ui-lab/src/internal/svg-icons}/Clock.tsx (88%) rename packages/{pickers/lib/src/_shared/icons => material-ui-lab/src/internal/svg-icons}/DateRange.tsx (84%) rename packages/{pickers/lib/src/_shared/icons => material-ui-lab/src/internal/svg-icons}/Pen.tsx (86%) rename packages/{pickers/lib/src/_shared/icons => material-ui-lab/src/internal/svg-icons}/Time.tsx (89%) delete mode 100644 packages/pickers/.circleci/config.yml delete mode 100644 packages/pickers/.dependabot/config.yml delete mode 100644 packages/pickers/.editorconfig delete mode 100644 packages/pickers/.eslintrc.js delete mode 100644 packages/pickers/.gitattributes delete mode 100644 packages/pickers/.github/FUNDING.yml delete mode 100644 packages/pickers/.github/ISSUE_TEMPLATE/1.bug.md delete mode 100644 packages/pickers/.github/ISSUE_TEMPLATE/2.feature.md delete mode 100644 packages/pickers/.github/ISSUE_TEMPLATE/3.material-design.md delete mode 100644 packages/pickers/.github/ISSUE_TEMPLATE/config.yml delete mode 100644 packages/pickers/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 packages/pickers/.gitignore delete mode 100644 packages/pickers/.npmignore delete mode 100644 packages/pickers/.percy.yml delete mode 100644 packages/pickers/.prettierignore delete mode 100644 packages/pickers/.prettierrc delete mode 100644 packages/pickers/.vscode/launch.json delete mode 100644 packages/pickers/.vscode/settings.json delete mode 100644 packages/pickers/.vscode/tasks.json delete mode 100644 packages/pickers/CHANGELOG.md delete mode 100644 packages/pickers/CONTRIBUTING.md delete mode 100644 packages/pickers/LICENSE delete mode 100644 packages/pickers/README.md delete mode 100644 packages/pickers/cypress.json delete mode 100644 packages/pickers/cypress/fixtures/example.json delete mode 100644 packages/pickers/docs/.gitignore delete mode 100644 packages/pickers/docs/_constants.ts delete mode 100644 packages/pickers/docs/_shared/Ad.tsx delete mode 100644 packages/pickers/docs/_shared/Code.tsx delete mode 100644 packages/pickers/docs/_shared/Example.tsx delete mode 100644 packages/pickers/docs/_shared/LinkedComponents.tsx delete mode 100644 packages/pickers/docs/_shared/PageMeta.tsx delete mode 100644 packages/pickers/docs/_shared/PatreonSponsors.jsx delete mode 100644 packages/pickers/docs/_shared/PropTypesTable.tsx delete mode 100644 packages/pickers/docs/_shared/UtilsServiceContext.tsx delete mode 100644 packages/pickers/docs/_shared/svgIcons/GithubIcon.jsx delete mode 100644 packages/pickers/docs/_shared/svgIcons/KawaiiIcon.tsx delete mode 100644 packages/pickers/docs/_shared/svgIcons/LightbulbIcon.tsx delete mode 100644 packages/pickers/docs/_shared/svgIcons/Logo.tsx delete mode 100644 packages/pickers/docs/babel.config.js delete mode 100644 packages/pickers/docs/fakeApi/randomDate.ts delete mode 100644 packages/pickers/docs/layout/Layout.tsx delete mode 100644 packages/pickers/docs/layout/PageWithContext.tsx delete mode 100644 packages/pickers/docs/layout/components/DrawerMenu.tsx delete mode 100644 packages/pickers/docs/layout/components/NavItem.jsx delete mode 100644 packages/pickers/docs/layout/components/NavigationMenu.tsx delete mode 100644 packages/pickers/docs/layout/components/navigationMap.ts delete mode 100644 packages/pickers/docs/layout/styleOverrides.ts delete mode 100644 packages/pickers/docs/loaders/example-loader.js delete mode 100644 packages/pickers/docs/next.config.js delete mode 100644 packages/pickers/docs/notifications.json delete mode 100644 packages/pickers/docs/package.json delete mode 100644 packages/pickers/docs/pages/_app.tsx delete mode 100644 packages/pickers/docs/pages/_document.tsx delete mode 100644 packages/pickers/docs/pages/_error.tsx delete mode 100644 packages/pickers/docs/pages/api/props.tsx delete mode 100644 packages/pickers/docs/pages/demo/datepicker/BasicDatePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datepicker/CustomInput.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datepicker/DatePickers.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datepicker/ServerRequest.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datepicker/StaticDatePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datepicker/ViewsDatePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datepicker/index.mdx delete mode 100644 packages/pickers/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/daterangepicker/CalendarsDateRangePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/daterangepicker/CustomRangeInputs.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/daterangepicker/MinMaxDateRangePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/daterangepicker/StaticDateRangePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/daterangepicker/index.mdx delete mode 100644 packages/pickers/docs/pages/demo/datetime-picker/CustomDateTimePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datetime-picker/DateTimePickers.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datetime-picker/DateTimeValidation.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/datetime-picker/index.mdx delete mode 100644 packages/pickers/docs/pages/demo/timepicker/BasicTimePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/timepicker/SecondsTimePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/timepicker/StaticTimePicker.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/timepicker/TimePickers.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/timepicker/TimeValidation.example.tsx delete mode 100644 packages/pickers/docs/pages/demo/timepicker/index.mdx delete mode 100644 packages/pickers/docs/pages/getting-started/installation.mdx delete mode 100644 packages/pickers/docs/pages/getting-started/parsing.mdx delete mode 100644 packages/pickers/docs/pages/getting-started/usage.mdx delete mode 100644 packages/pickers/docs/pages/guides/Accessibility.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/ControllingProgrammatically.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/CssOverrides.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/CssTheme.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/CssThemeSpacing.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/DateAdapterProp.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/Formats.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/FormikOurValidation.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/FormikValidationSchema.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/OverrideLogic.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/StaticComponents.example.tsx delete mode 100644 packages/pickers/docs/pages/guides/accessibility.mdx delete mode 100644 packages/pickers/docs/pages/guides/controlling-programmatically.mdx delete mode 100644 packages/pickers/docs/pages/guides/css-overrides.mdx delete mode 100644 packages/pickers/docs/pages/guides/date-adapter-passing.mdx delete mode 100644 packages/pickers/docs/pages/guides/date-io-customization.mdx delete mode 100644 packages/pickers/docs/pages/guides/forms.mdx delete mode 100644 packages/pickers/docs/pages/guides/static-components.mdx delete mode 100644 packages/pickers/docs/pages/guides/typescript.mdx delete mode 100644 packages/pickers/docs/pages/guides/upgrading-to-v3.mdx delete mode 100644 packages/pickers/docs/pages/index/Landing.tsx delete mode 100644 packages/pickers/docs/pages/index/LandingProperty.tsx delete mode 100644 packages/pickers/docs/pages/index/index.tsx delete mode 100644 packages/pickers/docs/pages/localization/Date-fns.example.tsx delete mode 100644 packages/pickers/docs/pages/localization/Hijri.example.tsx delete mode 100644 packages/pickers/docs/pages/localization/Moment.example.tsx delete mode 100644 packages/pickers/docs/pages/localization/Persian.example.tsx delete mode 100644 packages/pickers/docs/pages/localization/calendar-systems.mdx delete mode 100644 packages/pickers/docs/pages/localization/date-fns.mdx delete mode 100644 packages/pickers/docs/pages/localization/moment.mdx delete mode 100644 packages/pickers/docs/pages/regression/Regression.tsx delete mode 100644 packages/pickers/docs/pages/regression/RegressionDay.tsx delete mode 100644 packages/pickers/docs/pages/regression/index.tsx delete mode 100644 packages/pickers/docs/pages/releases.tsx delete mode 100644 packages/pickers/docs/patrons.json delete mode 100644 packages/pickers/docs/prop-types.json delete mode 100644 packages/pickers/docs/scripts/assign-production-domains.js delete mode 100644 packages/pickers/docs/scripts/docgen.js delete mode 100644 packages/pickers/docs/scripts/generate-backers.js delete mode 100644 packages/pickers/docs/static/android-icon-144x144.png delete mode 100644 packages/pickers/docs/static/android-icon-192x192.png delete mode 100644 packages/pickers/docs/static/android-icon-36x36.png delete mode 100644 packages/pickers/docs/static/android-icon-48x48.png delete mode 100644 packages/pickers/docs/static/android-icon-72x72.png delete mode 100644 packages/pickers/docs/static/android-icon-96x96.png delete mode 100644 packages/pickers/docs/static/apple-icon-114x114.png delete mode 100644 packages/pickers/docs/static/apple-icon-120x120.png delete mode 100644 packages/pickers/docs/static/apple-icon-144x144.png delete mode 100644 packages/pickers/docs/static/apple-icon-152x152.png delete mode 100644 packages/pickers/docs/static/apple-icon-180x180.png delete mode 100644 packages/pickers/docs/static/apple-icon-57x57.png delete mode 100644 packages/pickers/docs/static/apple-icon-60x60.png delete mode 100644 packages/pickers/docs/static/apple-icon-72x72.png delete mode 100644 packages/pickers/docs/static/apple-icon-76x76.png delete mode 100644 packages/pickers/docs/static/apple-icon-precomposed.png delete mode 100644 packages/pickers/docs/static/apple-icon.png delete mode 100644 packages/pickers/docs/static/favicon-16x16.png delete mode 100644 packages/pickers/docs/static/favicon-32x32.png delete mode 100644 packages/pickers/docs/static/favicon-96x96.png delete mode 100644 packages/pickers/docs/static/favicon.ico delete mode 100644 packages/pickers/docs/static/manifest.json delete mode 100644 packages/pickers/docs/static/meta-image.png delete mode 100644 packages/pickers/docs/static/ms-icon-144x144.png delete mode 100644 packages/pickers/docs/static/ms-icon-150x150.png delete mode 100644 packages/pickers/docs/static/ms-icon-310x310.png delete mode 100644 packages/pickers/docs/static/ms-icon-70x70.png delete mode 100644 packages/pickers/docs/tsconfig.json delete mode 100644 packages/pickers/docs/typings.d.ts delete mode 100644 packages/pickers/docs/utils/NotificationManager.tsx delete mode 100644 packages/pickers/docs/utils/anchor-autolink.js delete mode 100644 packages/pickers/docs/utils/getPageContext.ts delete mode 100644 packages/pickers/docs/utils/github-api.ts delete mode 100644 packages/pickers/docs/utils/helpers.ts delete mode 100644 packages/pickers/docs/utils/prism.ts delete mode 100644 packages/pickers/docs/utils/table-styler.js delete mode 100644 packages/pickers/docs/utils/utilsService.ts delete mode 100644 packages/pickers/e2e/component/DatePicker.spec.tsx delete mode 100644 packages/pickers/e2e/component/DateTimePicker.spec.tsx delete mode 100644 packages/pickers/e2e/component/KeyboardNavigation.spec.tsx delete mode 100644 packages/pickers/e2e/integration/App.spec.ts delete mode 100644 packages/pickers/e2e/integration/DatePicker.spec.ts delete mode 100644 packages/pickers/e2e/integration/DateRange.spec.ts delete mode 100644 packages/pickers/e2e/integration/VisualRegression.spec.ts delete mode 100644 packages/pickers/e2e/plugins/index.js delete mode 100644 packages/pickers/e2e/support/commands.ts delete mode 100644 packages/pickers/e2e/support/index.js delete mode 100644 packages/pickers/e2e/test-utils.tsx delete mode 100644 packages/pickers/e2e/tsconfig.json delete mode 100644 packages/pickers/lib/.size-snapshot.json delete mode 100644 packages/pickers/lib/babel.config.js delete mode 100644 packages/pickers/lib/cypress.json delete mode 100644 packages/pickers/lib/jest.config.js delete mode 100644 packages/pickers/lib/package.json delete mode 100644 packages/pickers/lib/prepare-build-files.js delete mode 100644 packages/pickers/lib/remove-prop-types.js delete mode 100644 packages/pickers/lib/rollup.config.js delete mode 100644 packages/pickers/lib/src/CalendarSkeleton.tsx delete mode 100644 packages/pickers/lib/src/DatePicker/DatePicker.ts delete mode 100644 packages/pickers/lib/src/DatePicker/DatePickerToolbar.tsx delete mode 100644 packages/pickers/lib/src/DatePicker/index.ts delete mode 100644 packages/pickers/lib/src/DateRangePicker/DateRangePickerToolbar.tsx delete mode 100644 packages/pickers/lib/src/DateRangePicker/RangeTypes.ts delete mode 100644 packages/pickers/lib/src/DateTimePicker/DateTimePicker.tsx delete mode 100644 packages/pickers/lib/src/DateTimePicker/DateTimePickerToolbar.tsx delete mode 100644 packages/pickers/lib/src/DateTimePicker/index.ts delete mode 100644 packages/pickers/lib/src/LocalizationProvider.tsx delete mode 100644 packages/pickers/lib/src/Picker/SharedPickerProps.tsx delete mode 100644 packages/pickers/lib/src/TimePicker/TimePicker.tsx delete mode 100644 packages/pickers/lib/src/TimePicker/TimePickerToolbar.tsx delete mode 100644 packages/pickers/lib/src/TimePicker/index.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DatePicker.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DatePickerProps.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DatePickerRoot.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DatePickerTestingLib.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DateRangePicker.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DateRangePickerLegacy.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DateTimePicker.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DateTimePickerRoot.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/DateTimePickerTestingLib.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/KeyboardDatePicker.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/KeyboardDateTimePicker.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/Theme.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/TimePicker.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/Validation.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/commands.tsx delete mode 100644 packages/pickers/lib/src/__tests__/createClientRender.tsx delete mode 100644 packages/pickers/lib/src/__tests__/setup.ts delete mode 100644 packages/pickers/lib/src/__tests__/shallow/Month.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/shallow/MonthSelection.test.tsx delete mode 100644 packages/pickers/lib/src/__tests__/test-utils.tsx delete mode 100644 packages/pickers/lib/src/__tests__/tsconfig.json delete mode 100644 packages/pickers/lib/src/__tests__/typescript/GenericValues.spec.tsx delete mode 100644 packages/pickers/lib/src/__tests__/typescript/Overrides.tstest.tsx delete mode 100644 packages/pickers/lib/src/__tests__/typescript/ThemeDefaultProps.tstest.tsx delete mode 100644 packages/pickers/lib/src/__tests__/typings.d.ts delete mode 100644 packages/pickers/lib/src/__tests__/unit/date-range-manager.test.ts delete mode 100644 packages/pickers/lib/src/__tests__/unit/date-utils.test.ts delete mode 100644 packages/pickers/lib/src/__tests__/unit/text-field-helper.test.ts delete mode 100644 packages/pickers/lib/src/_shared/ToolbarText.tsx delete mode 100644 packages/pickers/lib/src/_shared/hooks/useTraceUpdate.tsx delete mode 100644 packages/pickers/lib/src/_shared/hooks/useViews.tsx delete mode 100644 packages/pickers/lib/src/constants/prop-types.ts delete mode 100644 packages/pickers/lib/src/index.ts delete mode 100644 packages/pickers/lib/src/typings/index.ts delete mode 100644 packages/pickers/lib/src/typings/overrides.ts delete mode 100644 packages/pickers/lib/src/typings/props.ts delete mode 100644 packages/pickers/lib/src/views/Calendar/CalendarView.tsx delete mode 100644 packages/pickers/lib/src/views/Calendar/MonthSelection.tsx delete mode 100644 packages/pickers/lib/src/views/Calendar/SlideTransition.tsx delete mode 100644 packages/pickers/lib/src/views/Calendar/Year.tsx delete mode 100644 packages/pickers/lib/src/views/Calendar/YearSelection.tsx delete mode 100644 packages/pickers/lib/src/views/MobileKeyboardInputView.tsx delete mode 100644 packages/pickers/lib/src/wrappers/Wrapper.tsx delete mode 100644 packages/pickers/lib/src/wrappers/WrapperVariantContext.tsx delete mode 100644 packages/pickers/lib/tsconfig.adapters.json delete mode 100644 packages/pickers/lib/tsconfig.json delete mode 100644 packages/pickers/lib/typings.d.ts delete mode 100644 packages/pickers/now.json delete mode 100644 packages/pickers/package.json delete mode 100644 packages/pickers/scripts/deduplicate.js delete mode 100644 packages/pickers/yarn.lock 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: ( - -