From 0cb20a005556b2e31ead8eddd9f4b8942384deab Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 27 Apr 2023 18:56:55 +0300 Subject: [PATCH] [pickers] Add new `DigitalClock` desktop time picking experience (#7958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Co-authored-by: José Rodolfo Freitas Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> --- .../digital-clock/DigitalClockAmPm.js | 67 ++ .../digital-clock/DigitalClockAmPm.tsx | 67 ++ .../digital-clock/DigitalClockBasic.js | 21 + .../digital-clock/DigitalClockBasic.tsx | 21 + .../DigitalClockBasic.tsx.preview | 6 + .../digital-clock/DigitalClockFormProps.js | 47 ++ .../digital-clock/DigitalClockFormProps.tsx | 47 ++ .../digital-clock/DigitalClockSkipDisabled.js | 42 ++ .../DigitalClockSkipDisabled.tsx | 43 ++ .../DigitalClockSkipDisabled.tsx.preview | 14 + .../digital-clock/DigitalClockTimeStep.js | 24 + .../digital-clock/DigitalClockTimeStep.tsx | 24 + .../DigitalClockTimeStep.tsx.preview | 9 + .../digital-clock/DigitalClockValue.js | 49 ++ .../digital-clock/DigitalClockValue.tsx | 49 ++ .../digital-clock/DigitalClockViews.js | 29 + .../digital-clock/DigitalClockViews.tsx | 29 + .../DigitalClockViews.tsx.preview | 9 + .../digital-clock/digital-clock.md | 83 +++ docs/data/date-pickers/localization/data.json | 52 +- .../date-pickers/time-clock/time-clock.md | 6 +- .../date-pickers/time-picker/time-picker.md | 9 +- .../date-pickers/validation/validation.md | 8 +- .../MobileKeyboardView.js | 1 + .../migration-pickers-v5.md | 8 +- docs/data/pages.ts | 10 + .../api/date-pickers/desktop-time-picker.json | 26 +- .../pages/x/api/date-pickers/digital-clock.js | 23 + .../x/api/date-pickers/digital-clock.json | 69 +++ docs/pages/x/api/date-pickers/index.md | 2 + .../multi-section-digital-clock.js | 23 + .../multi-section-digital-clock.json | 95 +++ docs/pages/x/api/date-pickers/time-clock.json | 13 +- .../api/date-pickers/time-picker-toolbar.json | 2 +- .../pages/x/api/date-pickers/time-picker.json | 26 +- .../x/react-date-pickers/digital-clock.js | 7 + .../date-pickers/desktop-time-picker.json | 5 + .../api-docs/date-pickers/digital-clock.json | 48 ++ .../multi-section-digital-clock.json | 38 ++ .../api-docs/date-pickers/time-clock.json | 18 +- .../api-docs/date-pickers/time-picker.json | 5 + .../dateRangeViewRenderers.tsx | 4 +- .../useDesktopRangePicker.tsx | 4 +- .../useDesktopRangePicker.types.ts | 18 +- .../hooks/useEnrichedRangePickerFieldProps.ts | 19 +- .../useMobileRangePicker.tsx | 4 +- .../useMobileRangePicker.types.ts | 14 +- .../useStaticRangePicker.tsx | 4 +- .../useStaticRangePicker.types.ts | 14 +- .../src/DatePicker/DatePickerToolbar.tsx | 4 +- .../src/DateTimePicker/shared.tsx | 9 +- .../DesktopTimePicker/DesktopTimePicker.tsx | 77 ++- .../DesktopTimePicker.types.ts | 29 +- .../tests/DesktopTimePicker.test.tsx | 160 +++++ .../describes.DesktopTimePicker.test.tsx | 30 +- .../src/DigitalClock/DigitalClock.tsx | 464 ++++++++++++++ .../src/DigitalClock/DigitalClock.types.ts | 57 ++ .../src/DigitalClock/digitalClockClasses.ts | 23 + .../x-date-pickers/src/DigitalClock/index.ts | 10 + .../tests/describes.DigitalClock.test.tsx | 75 +++ .../src/MobileTimePicker/MobileTimePicker.tsx | 9 +- .../MobileTimePicker.types.ts | 29 +- .../MultiSectionDigitalClock.tsx | 579 ++++++++++++++++++ .../MultiSectionDigitalClock.types.ts | 69 +++ .../MultiSectionDigitalClock.utils.ts | 98 +++ .../MultiSectionDigitalClockSection.tsx | 201 ++++++ .../src/MultiSectionDigitalClock/index.ts | 21 + .../multiSectionDigitalClockClasses.ts | 16 + .../multiSectionDigitalClockSectionClasses.ts | 18 + ...escribes.MultiSectionDigitalClock.test.tsx | 90 +++ .../src/PickersLayout/PickersLayout.tsx | 14 +- .../src/PickersLayout/PickersLayout.types.ts | 29 +- .../src/PickersLayout/usePickerLayout.tsx | 13 +- .../src/StaticTimePicker/StaticTimePicker.tsx | 9 +- .../StaticTimePicker.types.ts | 2 +- .../src/TimeClock/TimeClock.tsx | 42 +- .../src/TimeClock/TimeClock.types.ts | 75 +-- .../src/TimePicker/TimePicker.tsx | 30 +- .../src/TimePicker/TimePicker.types.ts | 7 +- .../src/TimePicker/TimePickerToolbar.tsx | 12 +- .../x-date-pickers/src/TimePicker/shared.tsx | 34 +- .../dateViewRenderers/dateViewRenderers.tsx | 5 +- packages/x-date-pickers/src/index.ts | 4 + .../internals/components/PickersToolbar.tsx | 8 +- .../src/internals/constants/dimensions.ts | 1 + .../src/internals/demo/DemoContainer.tsx | 7 +- .../internals/hooks/date-helpers-hooks.tsx | 6 +- .../useDesktopPicker/useDesktopPicker.tsx | 6 +- .../useDesktopPicker.types.ts | 26 +- .../src/internals/hooks/useIsLandscape.tsx | 4 +- .../hooks/useMobilePicker/useMobilePicker.tsx | 5 +- .../useMobilePicker/useMobilePicker.types.ts | 26 +- .../internals/hooks/usePicker/usePicker.ts | 5 +- .../hooks/usePicker/usePicker.types.ts | 11 +- .../hooks/usePicker/usePickerLayoutProps.ts | 15 +- .../hooks/usePicker/usePickerViews.ts | 22 +- .../hooks/useStaticPicker/useStaticPicker.tsx | 5 +- .../useStaticPicker/useStaticPicker.types.ts | 15 +- .../src/internals/hooks/useViews.tsx | 66 +- .../src/internals/models/common.ts | 6 + .../src/internals/models/index.ts | 1 + .../models/props/basePickerProps.tsx | 12 +- .../src/internals/models/props/clock.ts | 103 ++++ .../src/internals/models/props/tabs.ts | 4 +- .../src/internals/models/props/toolbar.ts | 6 +- .../src/internals/utils/time-utils.ts | 14 +- .../src/internals/utils/views.ts | 5 +- packages/x-date-pickers/src/locales/beBY.ts | 10 +- packages/x-date-pickers/src/locales/caES.ts | 7 +- packages/x-date-pickers/src/locales/csCZ.ts | 9 +- packages/x-date-pickers/src/locales/daDK.ts | 7 +- packages/x-date-pickers/src/locales/deDE.ts | 7 +- packages/x-date-pickers/src/locales/enUS.ts | 3 + packages/x-date-pickers/src/locales/esES.ts | 7 +- packages/x-date-pickers/src/locales/faIR.ts | 13 +- packages/x-date-pickers/src/locales/fiFI.ts | 7 +- packages/x-date-pickers/src/locales/frFR.ts | 7 +- packages/x-date-pickers/src/locales/heIL.ts | 7 +- packages/x-date-pickers/src/locales/huHU.ts | 7 +- packages/x-date-pickers/src/locales/isIS.ts | 13 +- packages/x-date-pickers/src/locales/itIT.ts | 7 +- packages/x-date-pickers/src/locales/jaJP.ts | 7 +- packages/x-date-pickers/src/locales/koKR.ts | 7 +- packages/x-date-pickers/src/locales/kzKZ.ts | 7 +- packages/x-date-pickers/src/locales/nbNO.ts | 13 +- packages/x-date-pickers/src/locales/nlNL.ts | 16 +- packages/x-date-pickers/src/locales/plPL.ts | 13 +- packages/x-date-pickers/src/locales/ptBR.ts | 13 +- packages/x-date-pickers/src/locales/ruRU.ts | 7 +- packages/x-date-pickers/src/locales/svSE.ts | 13 +- packages/x-date-pickers/src/locales/trTR.ts | 13 +- packages/x-date-pickers/src/locales/ukUA.ts | 13 +- packages/x-date-pickers/src/locales/urPK.ts | 13 +- .../src/locales/utils/pickersLocaleTextApi.ts | 4 + packages/x-date-pickers/src/locales/zhCN.ts | 9 +- packages/x-date-pickers/src/models/common.ts | 5 + packages/x-date-pickers/src/models/index.ts | 1 + .../src/tests/describe.types.ts | 9 +- .../testControlledUnControlled.tsx | 21 +- .../describeValue/testPickerActionBar.tsx | 7 +- .../testPickerOpenCloseLifeCycle.tsx | 5 - .../src/themeAugmentation/components.d.ts | 12 + .../src/themeAugmentation/overrides.d.ts | 8 + .../src/themeAugmentation/props.d.ts | 8 + .../themeAugmentation.spec.ts | 62 ++ .../src/timeViewRenderers/index.ts | 6 +- .../timeViewRenderers/timeViewRenderers.tsx | 168 ++++- scripts/x-date-pickers-pro.exports.json | 26 +- scripts/x-date-pickers.exports.json | 26 +- test/utils/pickers-utils.tsx | 12 + 150 files changed, 4153 insertions(+), 460 deletions(-) create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockAmPm.js create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockAmPm.tsx create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockBasic.js create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockBasic.tsx create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockBasic.tsx.preview create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockFormProps.js create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockFormProps.tsx create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.js create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.tsx create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.tsx.preview create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockTimeStep.js create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockTimeStep.tsx create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockTimeStep.tsx.preview create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockValue.js create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockValue.tsx create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockViews.js create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockViews.tsx create mode 100644 docs/data/date-pickers/digital-clock/DigitalClockViews.tsx.preview create mode 100644 docs/data/date-pickers/digital-clock/digital-clock.md create mode 100644 docs/pages/x/api/date-pickers/digital-clock.js create mode 100644 docs/pages/x/api/date-pickers/digital-clock.json create mode 100644 docs/pages/x/api/date-pickers/multi-section-digital-clock.js create mode 100644 docs/pages/x/api/date-pickers/multi-section-digital-clock.json create mode 100644 docs/pages/x/react-date-pickers/digital-clock.js create mode 100644 docs/translations/api-docs/date-pickers/digital-clock.json create mode 100644 docs/translations/api-docs/date-pickers/multi-section-digital-clock.json create mode 100644 packages/x-date-pickers/src/DesktopTimePicker/tests/DesktopTimePicker.test.tsx create mode 100644 packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx create mode 100644 packages/x-date-pickers/src/DigitalClock/DigitalClock.types.ts create mode 100644 packages/x-date-pickers/src/DigitalClock/digitalClockClasses.ts create mode 100644 packages/x-date-pickers/src/DigitalClock/index.ts create mode 100644 packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.types.ts create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.utils.ts create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/index.ts create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/multiSectionDigitalClockClasses.ts create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/multiSectionDigitalClockSectionClasses.ts create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx create mode 100644 packages/x-date-pickers/src/internals/models/props/clock.ts create mode 100644 packages/x-date-pickers/src/models/common.ts diff --git a/docs/data/date-pickers/digital-clock/DigitalClockAmPm.js b/docs/data/date-pickers/digital-clock/DigitalClockAmPm.js new file mode 100644 index 000000000000..e3194a1d2904 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockAmPm.js @@ -0,0 +1,67 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import Typography from '@mui/material/Typography'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockAmPm() { + return ( + + + + + Locale default behavior (enabled for enUS) + + + + + + + + + + + + AM PM enabled + + + + + + + + + + + AM PM disabled + + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockAmPm.tsx b/docs/data/date-pickers/digital-clock/DigitalClockAmPm.tsx new file mode 100644 index 000000000000..e3194a1d2904 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockAmPm.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import Typography from '@mui/material/Typography'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockAmPm() { + return ( + + + + + Locale default behavior (enabled for enUS) + + + + + + + + + + + + AM PM enabled + + + + + + + + + + + AM PM disabled + + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockBasic.js b/docs/data/date-pickers/digital-clock/DigitalClockBasic.js new file mode 100644 index 000000000000..0280de136ee2 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockBasic.js @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockBasic() { + return ( + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockBasic.tsx b/docs/data/date-pickers/digital-clock/DigitalClockBasic.tsx new file mode 100644 index 000000000000..0280de136ee2 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockBasic.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockBasic() { + return ( + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockBasic.tsx.preview b/docs/data/date-pickers/digital-clock/DigitalClockBasic.tsx.preview new file mode 100644 index 000000000000..a7e2d0878a36 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockBasic.tsx.preview @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/data/date-pickers/digital-clock/DigitalClockFormProps.js b/docs/data/date-pickers/digital-clock/DigitalClockFormProps.js new file mode 100644 index 000000000000..3ad71c6fe6c6 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockFormProps.js @@ -0,0 +1,47 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockFormProps() { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockFormProps.tsx b/docs/data/date-pickers/digital-clock/DigitalClockFormProps.tsx new file mode 100644 index 000000000000..3ad71c6fe6c6 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockFormProps.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockFormProps() { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.js b/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.js new file mode 100644 index 000000000000..240a8f343ad8 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.js @@ -0,0 +1,42 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +const shouldDisableTime = (value, view) => { + const hour = value.hour(); + if (view === 'hours') { + return hour < 9 || hour > 13; + } + if (view === 'minutes') { + const minute = value.minute(); + return minute > 20 && hour === 13; + } + return false; +}; + +export default function DigitalClockSkipDisabled() { + return ( + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.tsx b/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.tsx new file mode 100644 index 000000000000..cd8c8127fc04 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; +import { TimeView } from '@mui/x-date-pickers/models'; + +const shouldDisableTime = (value: Dayjs, view: TimeView) => { + const hour = value.hour(); + if (view === 'hours') { + return hour < 9 || hour > 13; + } + if (view === 'minutes') { + const minute = value.minute(); + return minute > 20 && hour === 13; + } + return false; +}; + +export default function DigitalClockSkipDisabled() { + return ( + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.tsx.preview b/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.tsx.preview new file mode 100644 index 000000000000..cae247f4015f --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockSkipDisabled.tsx.preview @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.js b/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.js new file mode 100644 index 000000000000..2889cf421b01 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.js @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockTimeStep() { + return ( + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.tsx b/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.tsx new file mode 100644 index 000000000000..2889cf421b01 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockTimeStep() { + return ( + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.tsx.preview b/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.tsx.preview new file mode 100644 index 000000000000..4a15774bbd2b --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockTimeStep.tsx.preview @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/docs/data/date-pickers/digital-clock/DigitalClockValue.js b/docs/data/date-pickers/digital-clock/DigitalClockValue.js new file mode 100644 index 000000000000..8ab7c3bb6606 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockValue.js @@ -0,0 +1,49 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockValue() { + const [value, setValue] = React.useState(dayjs('2022-04-17T15:30')); + + return ( + + + + + + + + setValue(newValue)} + /> + + + + + + + + setValue(newValue)} + /> + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockValue.tsx b/docs/data/date-pickers/digital-clock/DigitalClockValue.tsx new file mode 100644 index 000000000000..882d0bd52b0b --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockValue.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockValue() { + const [value, setValue] = React.useState(dayjs('2022-04-17T15:30')); + + return ( + + + + + + + + setValue(newValue)} + /> + + + + + + + + setValue(newValue)} + /> + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockViews.js b/docs/data/date-pickers/digital-clock/DigitalClockViews.js new file mode 100644 index 000000000000..ae39d36e2d1f --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockViews.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockViews() { + return ( + + + + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockViews.tsx b/docs/data/date-pickers/digital-clock/DigitalClockViews.tsx new file mode 100644 index 000000000000..ae39d36e2d1f --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockViews.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { DemoContainer, DemoItem } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; + +export default function DigitalClockViews() { + return ( + + + + + + + + + + + + + + ); +} diff --git a/docs/data/date-pickers/digital-clock/DigitalClockViews.tsx.preview b/docs/data/date-pickers/digital-clock/DigitalClockViews.tsx.preview new file mode 100644 index 000000000000..8afb427adfef --- /dev/null +++ b/docs/data/date-pickers/digital-clock/DigitalClockViews.tsx.preview @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/docs/data/date-pickers/digital-clock/digital-clock.md b/docs/data/date-pickers/digital-clock/digital-clock.md new file mode 100644 index 000000000000..5a07a64eb7f4 --- /dev/null +++ b/docs/data/date-pickers/digital-clock/digital-clock.md @@ -0,0 +1,83 @@ +--- +product: date-pickers +title: React Digital Clock component +components: DigitalClock, MultiSectionDigitalClock +githubLabel: 'component: TimePicker' +packageName: '@mui/x-date-pickers' +--- + +# Digital Clock + +

The Digital Clock components let the user select a time without any input or popper / modal.

+ +## Description + +There are two component versions for different cases. The `DigitalClock` handles selection of a single time instance in one step, just like a `select` component. The `MultiSectionDigitalClock` allows selecting time using separate sections for separate views. + +The `DigitalClock` is more appropriate when there is a limited amount of time options needed, while the `MultiSectionDigitalClock` is suited for cases when a more granular time selection is needed. + +## Basic usage + +{{"demo": "DigitalClockBasic.js"}} + +## Uncontrolled vs. Controlled + +The components can be uncontrolled or controlled. + +{{"demo": "DigitalClockValue.js"}} + +## Form props + +The components can be disabled or read-only. + +{{"demo": "DigitalClockFormProps.js"}} + +## Views + +The `MultiSectionDigitalClock` component can contain three views: `hours`, `minutes`, and `seconds`. +By default, only the `hours` and `minutes` views are enabled. + +You can customize the enabled views using the `views` prop. +Views will appear in the order they're included in the `views` array. + +{{"demo": "DigitalClockViews.js"}} + +## 12h/24h format + +The components use the hour format of the locale's time setting, i.e. the 12-hour or 24-hour format. + +You can force a specific format using the `ampm` prop. + +You can find more information about 12h/24h format in the [Date localization page](/x/react-date-pickers/adapters-locale/#12h-24h-format). + +{{"demo": "DigitalClockAmPm.js"}} + +## Time steps + +By default, the components list the time options in the following way: + +- `DigitalClock` in `30` minutes intervals; +- `MultiSectionDigitalClock` component in `5` unit (`minutes` or `seconds`) intervals; + +You can set the desired interval using the `timeStep` and `timeSteps` props. +The prop accepts: + +- The `DigitalClock` component accepts a `number` value `timeStep` prop; +- The `MultiSectionDigitalClock` component accepts a `timeSteps` prop with `number` values for `hours`, `minutes`, or `seconds` units; + +{{"demo": "DigitalClockTimeStep.js"}} + +## Skip rendering disabled options + +With the `skipDisabled` prop, the components don't render options that are not available to the user (e.g. through `minTime`, `maxTime`, `shouldDisabledTime` etc.). + +The following example combines these properties to customize which options are rendered. + +- The first component does not show options before `9:00` (the value of `minTime`). +- The second one shows options between `09:00` and `13:20` thanks to `shouldDisableTime`. + +{{"demo": "DigitalClockSkipDisabled.js"}} + +## Validation + +You can find the documentation in the [Validation page](/x/react-date-pickers/validation/). diff --git a/docs/data/date-pickers/localization/data.json b/docs/data/date-pickers/localization/data.json index 178dcad0c53b..b34a802d8b66 100644 --- a/docs/data/date-pickers/localization/data.json +++ b/docs/data/date-pickers/localization/data.json @@ -4,7 +4,7 @@ "importName": "beBY", "localeName": "Belarusian", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/beBY.ts" }, { @@ -12,7 +12,7 @@ "importName": "caES", "localeName": "Catalan", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/caES.ts" }, { @@ -20,7 +20,7 @@ "importName": "zhCN", "localeName": "Chinese (Simplified)", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/zhCN.ts" }, { @@ -28,7 +28,7 @@ "importName": "csCZ", "localeName": "Czech", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/csCZ.ts" }, { @@ -36,7 +36,7 @@ "importName": "daDK", "localeName": "Danish", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/daDK.ts" }, { @@ -44,7 +44,7 @@ "importName": "nlNL", "localeName": "Dutch", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/nlNL.ts" }, { @@ -52,7 +52,7 @@ "importName": "fiFI", "localeName": "Finnish", "missingKeysCount": 12, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/fiFI.ts" }, { @@ -60,7 +60,7 @@ "importName": "frFR", "localeName": "French", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/frFR.ts" }, { @@ -68,7 +68,7 @@ "importName": "deDE", "localeName": "German", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/deDE.ts" }, { @@ -76,7 +76,7 @@ "importName": "heIL", "localeName": "Hebrew", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/heIL.ts" }, { @@ -84,7 +84,7 @@ "importName": "huHU", "localeName": "Hungarian", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/huHU.ts" }, { @@ -92,7 +92,7 @@ "importName": "isIS", "localeName": "Icelandic", "missingKeysCount": 12, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/isIS.ts" }, { @@ -100,7 +100,7 @@ "importName": "itIT", "localeName": "Italian", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/itIT.ts" }, { @@ -108,7 +108,7 @@ "importName": "jaJP", "localeName": "Japanese", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/jaJP.ts" }, { @@ -116,7 +116,7 @@ "importName": "kzKZ", "localeName": "Kazakh", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/kzKZ.ts" }, { @@ -124,7 +124,7 @@ "importName": "koKR", "localeName": "Korean", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/koKR.ts" }, { @@ -132,7 +132,7 @@ "importName": "nbNO", "localeName": "Norwegian (bokmål)", "missingKeysCount": 8, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/nbNO.ts" }, { @@ -140,7 +140,7 @@ "importName": "faIR", "localeName": "Persian", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/faIR.ts" }, { @@ -148,7 +148,7 @@ "importName": "plPL", "localeName": "Polish", "missingKeysCount": 8, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/plPL.ts" }, { @@ -156,7 +156,7 @@ "importName": "ptBR", "localeName": "Portuguese (Brazil)", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/ptBR.ts" }, { @@ -164,7 +164,7 @@ "importName": "ruRU", "localeName": "Russian", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/ruRU.ts" }, { @@ -172,7 +172,7 @@ "importName": "esES", "localeName": "Spanish", "missingKeysCount": 0, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/esES.ts" }, { @@ -180,7 +180,7 @@ "importName": "svSE", "localeName": "Swedish", "missingKeysCount": 8, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/svSE.ts" }, { @@ -188,7 +188,7 @@ "importName": "trTR", "localeName": "Turkish", "missingKeysCount": 5, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/trTR.ts" }, { @@ -196,7 +196,7 @@ "importName": "ukUA", "localeName": "Ukrainian", "missingKeysCount": 1, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/ukUA.ts" }, { @@ -204,7 +204,7 @@ "importName": "urPK", "localeName": "Urdu (Pakistan)", "missingKeysCount": 8, - "totalKeysCount": 35, + "totalKeysCount": 36, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/urPK.ts" } ] diff --git a/docs/data/date-pickers/time-clock/time-clock.md b/docs/data/date-pickers/time-clock/time-clock.md index 24e8433f3801..1a4b1169acb7 100644 --- a/docs/data/date-pickers/time-clock/time-clock.md +++ b/docs/data/date-pickers/time-clock/time-clock.md @@ -38,10 +38,14 @@ Views will appear in the order they're included in the `views` array. ## 12h/24h format -By default, the component uses the hour format of the locale's time setting, i.e. the 12-hour or 24-hour format. +The component uses the hour format of the locale's time setting, i.e. the 12-hour or 24-hour format. You can force a specific format using the `ampm` prop. You can find more information about 12h/24h format in the [Date localization page](/x/react-date-pickers/adapters-locale/#12h-24h-format). {{"demo": "TimeClockAmPm.js"}} + +## Validation + +You can find the documentation in the [Validation page](/x/react-date-pickers/validation/). diff --git a/docs/data/date-pickers/time-picker/time-picker.md b/docs/data/date-pickers/time-picker/time-picker.md index c765fdf79c62..9a5f6051c0ca 100644 --- a/docs/data/date-pickers/time-picker/time-picker.md +++ b/docs/data/date-pickers/time-picker/time-picker.md @@ -11,24 +11,19 @@ materialDesign: https://m2.material.io/components/time-pickers

The Time Picker component lets the user select a time.

-:::info -The component by default currently does not ship with **time** picker view experience on **desktop**. -It was a conscious decision and a first step towards having a more user friendly desktop experience [discussed in #4483](https://github.com/mui/mui-x/issues/4483). -If a desktop view experience is essential, you can revert to it by following the suggestion [in the migration guide](/x/migration/migration-pickers-v5/#stop-rendering-a-clock-on-desktop). -::: - ## Basic usage {{"demo": "BasicTimePicker.js"}} ## Component composition -The component is built using the `TimeField` for the keyboard editing and the `TimeClock` for the view editing. +The component is built using the `TimeField` for the keyboard editing, the `DigitalClock` for the desktop view editing, and the `TimeClock` for the mobile view editing. All the documented props of those two components can also be passed to the Time Picker component. Check-out their documentation page for more information: - [Time Field](/x/react-date-pickers/time-field/) +- [Digital Clock](/x/react-date-pickers/digital-clock/) - [Time Clock](/x/react-date-pickers/time-clock/) ## Uncontrolled vs. Controlled diff --git a/docs/data/date-pickers/validation/validation.md b/docs/data/date-pickers/validation/validation.md index c5a1abfa9a66..bd9bce42bf61 100644 --- a/docs/data/date-pickers/validation/validation.md +++ b/docs/data/date-pickers/validation/validation.md @@ -18,7 +18,7 @@ The validation props are showcased for each type of picker component using the r But the same props are available on: -- all the other variants of this picker +- all the other variants of this picker; For example—the validation props showcased with `DatePicker` are also available on: @@ -26,9 +26,13 @@ But the same props are available on: - `MobileDatePicker` - `StaticDatePicker` -- the field used by this picker +- the field used by this picker; For example—the validation props showcased with `DatePicker` are also available on `DateField`. + +- the view components; + + For example—the validation props showcased with `TimePicker` are also available on `TimeClock` and `DigitalClock`. ::: ## Invalid values feedback diff --git a/docs/data/migration/migration-pickers-v5/MobileKeyboardView.js b/docs/data/migration/migration-pickers-v5/MobileKeyboardView.js index a55ff54f4649..b8b0683c4e9d 100644 --- a/docs/data/migration/migration-pickers-v5/MobileKeyboardView.js +++ b/docs/data/migration/migration-pickers-v5/MobileKeyboardView.js @@ -153,6 +153,7 @@ LayoutWithKeyboardView.propTypes = { view: PropTypes.oneOf([ 'day', 'hours', + 'meridiem', 'minutes', 'month', 'seconds', diff --git a/docs/data/migration/migration-pickers-v5/migration-pickers-v5.md b/docs/data/migration/migration-pickers-v5/migration-pickers-v5.md index beb6d4eeb51b..7e792ec08892 100644 --- a/docs/data/migration/migration-pickers-v5/migration-pickers-v5.md +++ b/docs/data/migration/migration-pickers-v5/migration-pickers-v5.md @@ -112,13 +112,13 @@ import { DateTime } from 'luxon'; ### Stop rendering a clock on desktop -In desktop mode, the `DateTimePicker` and `TimePicker` components will not display the clock. -This is the first step towards moving to a [better implementation](https://github.com/mui/mui-x/issues/4483). +In desktop mode, the `DateTimePicker` and `TimePicker` components will no longer render the [`TimeClock`](/x/react-date-pickers/time-clock/) component. +The `DateTimePicker` component currently has no replacement, but on `TimePicker` a new [`DigitalClock`](/x/react-date-pickers/digital-clock/) component has been introduced instead. The behavior on mobile mode is still the same. If you were relying on Clock Picker in desktop mode for tests—make sure to check [testing caveats](/x/react-date-pickers/base-concepts/#testing-caveats) to choose the best replacement for it. -You can manually re-enable the clock using the new `viewRenderers` prop. -The code below enables the `Clock` UI on all the `DesktopTimePicker` and `DesktopDateTimePicker` in your application. +You can manually re-enable the previous clock component using the new `viewRenderers` prop. +The code below enables the `TimeClock` UI on all the `DesktopTimePicker` and `DesktopDateTimePicker` in your application. Take a look at the [default props via theme documentation](/material-ui/customization/theme-components/#theme-default-props) for more information. diff --git a/docs/data/pages.ts b/docs/data/pages.ts index 35c8373b8064..1ee6a4fe0d33 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -199,6 +199,11 @@ const pages: MuiPage[] = [ { pathname: '/x/react-date-pickers/time-picker', title: 'Time Picker' }, { pathname: '/x/react-date-pickers/time-field', title: 'Time Field', newFeature: true }, { pathname: '/x/react-date-pickers/time-clock', title: 'Time Clock' }, + { + pathname: '/x/react-date-pickers/digital-clock', + title: 'Digital Clock', + newFeature: true, + }, ], }, { @@ -338,6 +343,7 @@ const pages: MuiPage[] = [ title: 'DesktopDateTimePicker', }, { pathname: '/x/api/date-pickers/desktop-time-picker', title: 'DesktopTimePicker' }, + { pathname: '/x/api/date-pickers/digital-clock', title: 'DigitalClock' }, { pathname: '/x/api/date-pickers/localization-provider', title: 'LocalizationProvider' }, { pathname: '/x/api/date-pickers/mobile-date-picker', title: 'MobileDatePicker' }, { @@ -351,6 +357,10 @@ const pages: MuiPage[] = [ }, { pathname: '/x/api/date-pickers/mobile-time-picker', title: 'MobileTimePicker' }, { pathname: '/x/api/date-pickers/month-calendar', title: 'MonthCalendar' }, + { + pathname: '/x/api/date-pickers/multi-section-digital-clock', + title: 'MultiSectionDigitalClock', + }, { pathname: '/x/api/date-pickers/multi-input-date-range-field', title: 'MultiInputDateRangeField', diff --git a/docs/pages/x/api/date-pickers/desktop-time-picker.json b/docs/pages/x/api/date-pickers/desktop-time-picker.json index 1b13b6dfda58..088e97a37ff4 100644 --- a/docs/pages/x/api/date-pickers/desktop-time-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-time-picker.json @@ -50,7 +50,7 @@ "openTo": { "type": { "name": "enum", - "description": "'hours'
| 'minutes'
| 'seconds'" + "description": "'hours'
| 'meridiem'
| 'minutes'
| 'seconds'" } }, "orientation": { @@ -68,6 +68,7 @@ "deprecationInfo": "Consider using shouldDisableTime." }, "shouldDisableTime": { "type": { "name": "func" } }, + "skipDisabled": { "type": { "name": "bool" } }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, "default": "{}" }, "sx": { @@ -76,15 +77,26 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "thresholdToRenderTimeInASingleColumn": { "type": { "name": "number" }, "default": "24" }, + "timeSteps": { + "type": { + "name": "shape", + "description": "{ hours?: number, minutes?: number, seconds?: number }" + }, + "default": "{ hours: 1, minutes: 5, seconds: 5 }" + }, "value": { "type": { "name": "any" } }, "view": { "type": { "name": "enum", - "description": "'hours'
| 'minutes'
| 'seconds'" + "description": "'hours'
| 'meridiem'
| 'minutes'
| 'seconds'" } }, "viewRenderers": { - "type": { "name": "shape", "description": "{ hours?: func, minutes?: func, seconds?: func }" } + "type": { + "name": "shape", + "description": "{ hours?: func, meridiem?: func, minutes?: func, seconds?: func }" + } }, "views": { "type": { @@ -104,6 +116,14 @@ "default": "TrapFocus from @mui/material", "type": { "name": "elementType" } }, + "DigitalClockItem": { + "default": "MenuItem from '@mui/material'", + "type": { "name": "elementType" } + }, + "DigitalClockSectionItem": { + "default": "MenuItem from '@mui/material'", + "type": { "name": "elementType" } + }, "Field": { "type": { "name": "elementType" } }, "InputAdornment": { "default": "InputAdornment", "type": { "name": "elementType" } }, "Layout": { "type": { "name": "elementType" } }, diff --git a/docs/pages/x/api/date-pickers/digital-clock.js b/docs/pages/x/api/date-pickers/digital-clock.js new file mode 100644 index 000000000000..1fbd938a90ee --- /dev/null +++ b/docs/pages/x/api/date-pickers/digital-clock.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docsx/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './digital-clock.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docsx/translations/api-docs/date-pickers', + false, + /\.\/digital-clock(-[a-z]{2})?\.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/x/api/date-pickers/digital-clock.json b/docs/pages/x/api/date-pickers/digital-clock.json new file mode 100644 index 000000000000..35bd08fce8af --- /dev/null +++ b/docs/pages/x/api/date-pickers/digital-clock.json @@ -0,0 +1,69 @@ +{ + "props": { + "ampm": { "type": { "name": "bool" }, "default": "`utils.is12HourCycleInCurrentLocale()`" }, + "autoFocus": { "type": { "name": "bool" } }, + "classes": { "type": { "name": "object" } }, + "components": { + "type": { "name": "object" }, + "default": "{}", + "deprecated": true, + "deprecationInfo": "Please use slots." + }, + "componentsProps": { + "type": { "name": "object" }, + "default": "{}", + "deprecated": true, + "deprecationInfo": "Please use slotProps." + }, + "defaultValue": { "type": { "name": "any" } }, + "disabled": { "type": { "name": "bool" } }, + "disableFuture": { "type": { "name": "bool" } }, + "disableIgnoringDatePartForTimeValidation": { "type": { "name": "bool" } }, + "disablePast": { "type": { "name": "bool" } }, + "focusedView": { "type": { "name": "enum", "description": "'hours'" } }, + "maxTime": { "type": { "name": "any" } }, + "minTime": { "type": { "name": "any" } }, + "minutesStep": { "type": { "name": "number" }, "default": "1" }, + "onChange": { "type": { "name": "func" } }, + "onFocusedViewChange": { "type": { "name": "func" } }, + "onViewChange": { "type": { "name": "func" } }, + "openTo": { "type": { "name": "enum", "description": "'hours'" } }, + "readOnly": { "type": { "name": "bool" } }, + "shouldDisableClock": { + "type": { "name": "func" }, + "deprecated": true, + "deprecationInfo": "Consider using shouldDisableTime." + }, + "shouldDisableTime": { "type": { "name": "func" } }, + "skipDisabled": { "type": { "name": "bool" } }, + "slotProps": { "type": { "name": "object" }, "default": "{}" }, + "slots": { "type": { "name": "object" }, "default": "{}" }, + "sx": { + "type": { + "name": "union", + "description": "Array<func
| object
| bool>
| func
| object" + } + }, + "timeStep": { "type": { "name": "number" }, "default": "30" }, + "value": { "type": { "name": "any" } }, + "view": { "type": { "name": "enum", "description": "'hours'" } }, + "views": { "type": { "name": "arrayOf", "description": "Array<'hours'>" } } + }, + "slots": { + "DigitalClockItem": { + "default": "MenuItem from '@mui/material'", + "type": { "name": "elementType" } + } + }, + "name": "DigitalClock", + "styles": { "classes": ["root", "list", "item"], "globalClasses": {}, "name": "MuiDigitalClock" }, + "spread": false, + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx", + "inheritance": null, + "demos": "", + "packages": [ + { "packageName": "@mui/x-date-pickers-pro", "componentName": "DigitalClock" }, + { "packageName": "@mui/x-date-pickers", "componentName": "DigitalClock" } + ] +} diff --git a/docs/pages/x/api/date-pickers/index.md b/docs/pages/x/api/date-pickers/index.md index 7ae8f4e65792..0ff78f7890bf 100644 --- a/docs/pages/x/api/date-pickers/index.md +++ b/docs/pages/x/api/date-pickers/index.md @@ -54,6 +54,8 @@ ### Clocks - [TimeClock](/x/api/date-pickers/time-clock/) +- [DigitalClock](/x/api/date-pickers/digital-clock/) +- [MultiSectionDigitalClock](/x/api/date-pickers/multi-section-digital-clock/) ### Toolbars diff --git a/docs/pages/x/api/date-pickers/multi-section-digital-clock.js b/docs/pages/x/api/date-pickers/multi-section-digital-clock.js new file mode 100644 index 000000000000..abd631204d6d --- /dev/null +++ b/docs/pages/x/api/date-pickers/multi-section-digital-clock.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docsx/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './multi-section-digital-clock.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docsx/translations/api-docs/date-pickers', + false, + /\.\/multi-section-digital-clock(-[a-z]{2})?\.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/x/api/date-pickers/multi-section-digital-clock.json b/docs/pages/x/api/date-pickers/multi-section-digital-clock.json new file mode 100644 index 000000000000..e45abddd6340 --- /dev/null +++ b/docs/pages/x/api/date-pickers/multi-section-digital-clock.json @@ -0,0 +1,95 @@ +{ + "props": { + "ampm": { "type": { "name": "bool" }, "default": "`utils.is12HourCycleInCurrentLocale()`" }, + "autoFocus": { "type": { "name": "bool" } }, + "classes": { "type": { "name": "object" } }, + "components": { + "type": { "name": "object" }, + "default": "{}", + "deprecated": true, + "deprecationInfo": "Please use slots." + }, + "componentsProps": { + "type": { "name": "object" }, + "default": "{}", + "deprecated": true, + "deprecationInfo": "Please use slotProps." + }, + "defaultValue": { "type": { "name": "any" } }, + "disabled": { "type": { "name": "bool" } }, + "disableFuture": { "type": { "name": "bool" } }, + "disableIgnoringDatePartForTimeValidation": { "type": { "name": "bool" } }, + "disablePast": { "type": { "name": "bool" } }, + "focusedView": { + "type": { + "name": "enum", + "description": "'hours'
| 'meridiem'
| 'minutes'
| 'seconds'" + } + }, + "maxTime": { "type": { "name": "any" } }, + "minTime": { "type": { "name": "any" } }, + "minutesStep": { "type": { "name": "number" }, "default": "1" }, + "onChange": { "type": { "name": "func" } }, + "onFocusedViewChange": { "type": { "name": "func" } }, + "onViewChange": { "type": { "name": "func" } }, + "openTo": { + "type": { + "name": "enum", + "description": "'hours'
| 'meridiem'
| 'minutes'
| 'seconds'" + } + }, + "readOnly": { "type": { "name": "bool" } }, + "shouldDisableClock": { + "type": { "name": "func" }, + "deprecated": true, + "deprecationInfo": "Consider using shouldDisableTime." + }, + "shouldDisableTime": { "type": { "name": "func" } }, + "skipDisabled": { "type": { "name": "bool" } }, + "slotProps": { "type": { "name": "object" }, "default": "{}" }, + "slots": { "type": { "name": "object" }, "default": "{}" }, + "sx": { + "type": { + "name": "union", + "description": "Array<func
| object
| bool>
| func
| object" + } + }, + "timeSteps": { + "type": { + "name": "shape", + "description": "{ hours?: number, minutes?: number, seconds?: number }" + }, + "default": "{ hours: 1, minutes: 5, seconds: 5 }" + }, + "value": { "type": { "name": "any" } }, + "view": { + "type": { + "name": "enum", + "description": "'hours'
| 'meridiem'
| 'minutes'
| 'seconds'" + } + }, + "views": { + "type": { + "name": "arrayOf", + "description": "Array<'hours'
| 'meridiem'
| 'minutes'
| 'seconds'>" + } + } + }, + "slots": { + "DigitalClockSectionItem": { + "default": "MenuItem from '@mui/material'", + "type": { "name": "elementType" } + } + }, + "name": "MultiSectionDigitalClock", + "styles": { "classes": ["root"], "globalClasses": {}, "name": "MuiMultiSectionDigitalClock" }, + "spread": false, + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx", + "inheritance": null, + "demos": "", + "packages": [ + { "packageName": "@mui/x-date-pickers-pro", "componentName": "MultiSectionDigitalClock" }, + { "packageName": "@mui/x-date-pickers", "componentName": "MultiSectionDigitalClock" } + ] +} diff --git a/docs/pages/x/api/date-pickers/time-clock.json b/docs/pages/x/api/date-pickers/time-clock.json index 8afa65137915..4ae1f525df4c 100644 --- a/docs/pages/x/api/date-pickers/time-clock.json +++ b/docs/pages/x/api/date-pickers/time-clock.json @@ -21,17 +21,23 @@ "disableFuture": { "type": { "name": "bool" } }, "disableIgnoringDatePartForTimeValidation": { "type": { "name": "bool" } }, "disablePast": { "type": { "name": "bool" } }, + "focusedView": { + "type": { + "name": "enum", + "description": "'hours'
| 'minutes'
| 'seconds'" + } + }, "maxTime": { "type": { "name": "any" } }, "minTime": { "type": { "name": "any" } }, "minutesStep": { "type": { "name": "number" }, "default": "1" }, "onChange": { "type": { "name": "func" } }, + "onFocusedViewChange": { "type": { "name": "func" } }, "onViewChange": { "type": { "name": "func" } }, "openTo": { "type": { "name": "enum", "description": "'hours'
| 'minutes'
| 'seconds'" - }, - "default": "'hours'" + } }, "readOnly": { "type": { "name": "bool" } }, "shouldDisableClock": { @@ -59,8 +65,7 @@ "type": { "name": "arrayOf", "description": "Array<'hours'
| 'minutes'
| 'seconds'>" - }, - "default": "['hours', 'minutes']" + } } }, "slots": { diff --git a/docs/pages/x/api/date-pickers/time-picker-toolbar.json b/docs/pages/x/api/date-pickers/time-picker-toolbar.json index 0caae8dae2b8..c8d74c4d58e4 100644 --- a/docs/pages/x/api/date-pickers/time-picker-toolbar.json +++ b/docs/pages/x/api/date-pickers/time-picker-toolbar.json @@ -4,7 +4,7 @@ "view": { "type": { "name": "enum", - "description": "'hours'
| 'minutes'
| 'seconds'" + "description": "'hours'
| 'meridiem'
| 'minutes'
| 'seconds'" }, "required": true }, diff --git a/docs/pages/x/api/date-pickers/time-picker.json b/docs/pages/x/api/date-pickers/time-picker.json index 40dac8133db4..e4eb4001f3a9 100644 --- a/docs/pages/x/api/date-pickers/time-picker.json +++ b/docs/pages/x/api/date-pickers/time-picker.json @@ -54,7 +54,7 @@ "openTo": { "type": { "name": "enum", - "description": "'hours'
| 'minutes'
| 'seconds'" + "description": "'hours'
| 'meridiem'
| 'minutes'
| 'seconds'" } }, "orientation": { @@ -72,6 +72,7 @@ "deprecationInfo": "Consider using shouldDisableTime." }, "shouldDisableTime": { "type": { "name": "func" } }, + "skipDisabled": { "type": { "name": "bool" } }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, "default": "{}" }, "sx": { @@ -80,15 +81,26 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "thresholdToRenderTimeInASingleColumn": { "type": { "name": "number" }, "default": "24" }, + "timeSteps": { + "type": { + "name": "shape", + "description": "{ hours?: number, minutes?: number, seconds?: number }" + }, + "default": "{ hours: 1, minutes: 5, seconds: 5 }" + }, "value": { "type": { "name": "any" } }, "view": { "type": { "name": "enum", - "description": "'hours'
| 'minutes'
| 'seconds'" + "description": "'hours'
| 'meridiem'
| 'minutes'
| 'seconds'" } }, "viewRenderers": { - "type": { "name": "shape", "description": "{ hours?: func, minutes?: func, seconds?: func }" } + "type": { + "name": "shape", + "description": "{ hours?: func, meridiem?: func, minutes?: func, seconds?: func }" + } }, "views": { "type": { @@ -109,6 +121,14 @@ "type": { "name": "elementType" } }, "Dialog": { "default": "PickersModalDialogRoot", "type": { "name": "elementType" } }, + "DigitalClockItem": { + "default": "MenuItem from '@mui/material'", + "type": { "name": "elementType" } + }, + "DigitalClockSectionItem": { + "default": "MenuItem from '@mui/material'", + "type": { "name": "elementType" } + }, "Field": { "type": { "name": "elementType" } }, "InputAdornment": { "default": "InputAdornment", "type": { "name": "elementType" } }, "Layout": { "type": { "name": "elementType" } }, diff --git a/docs/pages/x/react-date-pickers/digital-clock.js b/docs/pages/x/react-date-pickers/digital-clock.js new file mode 100644 index 000000000000..ac943c96310b --- /dev/null +++ b/docs/pages/x/react-date-pickers/digital-clock.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/date-pickers/digital-clock/digital-clock.md?@mui/markdown'; + +export default function Page() { + return ; +} diff --git a/docs/translations/api-docs/date-pickers/desktop-time-picker.json b/docs/translations/api-docs/date-pickers/desktop-time-picker.json index 1695352224e7..06e38d42ca1c 100644 --- a/docs/translations/api-docs/date-pickers/desktop-time-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-time-picker.json @@ -35,9 +35,12 @@ "selectedSections": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally.", "shouldDisableClock": "Disable specific clock time.

Signature:
function(clockValue: number, view: TimeView) => boolean
clockValue: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "shouldDisableTime": "Disable specific time.

Signature:
function(value: TDate, view: TimeView) => boolean
value: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", + "skipDisabled": "If true, disabled digital clock items will not be rendered.", "slotProps": "The props used for each component slot.", "slots": "Overridable component slots.", "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "thresholdToRenderTimeInASingleColumn": "Amount of time options below or at which the single column time renderer is used.", + "timeSteps": "The time steps between two time unit options. For example, if timeStep.minutes = 8, then the available minute options will be [0, 8, 16, 24, 32, 40, 48, 56]. When single column time renderer is used, only timeStep.minutes will be used.", "value": "The selected value. Used when the component is controlled.", "view": "The visible view. Used when the component view is controlled. Must be a valid option from views list.", "viewRenderers": "Define custom view renderers for each section. If null, the section will only have field editing. If undefined, internally defined view will be the used.", @@ -49,6 +52,8 @@ "DesktopPaper": "Custom component for the paper rendered inside the desktop picker's Popper.", "DesktopTransition": "Custom component for the desktop popper Transition.", "DesktopTrapFocus": "Custom component for trapping the focus inside the views on desktop.", + "DigitalClockItem": "Component responsible for rendering a single digital clock item.", + "DigitalClockSectionItem": "Component responsible for rendering a single multi section digital clock section item.", "Field": "Component used to enter the date with the keyboard.", "InputAdornment": "Component displayed on the start or end input adornment used to open the picker on desktop.", "Layout": "Custom component for wrapping the layout.\nIt wraps the toolbar, views, action bar, and shortcuts.", diff --git a/docs/translations/api-docs/date-pickers/digital-clock.json b/docs/translations/api-docs/date-pickers/digital-clock.json new file mode 100644 index 000000000000..61c5e8af6edf --- /dev/null +++ b/docs/translations/api-docs/date-pickers/digital-clock.json @@ -0,0 +1,48 @@ +{ + "componentDescription": "", + "propDescriptions": { + "ampm": "12h/24h view for hour selection clock.", + "autoFocus": "If true, the main element is focused during the first mount. This main element is: - the element chosen by the visible view if any (i.e: the selected day on the day view). - the input element if there is a field rendered.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "components": "Overrideable components.", + "componentsProps": "The props used for each component slot.", + "defaultValue": "The default selected value. Used when the component is not controlled.", + "disabled": "If true, the picker views and text field are disabled.", + "disableFuture": "If true, disable values after the current date for date components, time for time components and both for date time components.", + "disableIgnoringDatePartForTimeValidation": "Do not ignore date part when validating min/max time.", + "disablePast": "If true, disable values before the current date for date components, time for time components and both for date time components.", + "focusedView": "Controlled focused view.", + "maxTime": "Maximal selectable time. The date part of the object will be ignored unless props.disableIgnoringDatePartForTimeValidation === true.", + "minTime": "Minimal selectable time. The date part of the object will be ignored unless props.disableIgnoringDatePartForTimeValidation === true.", + "minutesStep": "Step over minutes.", + "onChange": "Callback fired when the value changes.

Signature:
function(value: TDate | null, selectionState: PickerSelectionState | undefined, selectedView: TView | undefined) => void
value: The new value.
selectionState: Indicates if the date selection is complete.
selectedView: Indicates the view in which the selection has been made.", + "onFocusedViewChange": "Callback fired on focused view change.

Signature:
function(view: TView, hasFocus: boolean) => void
view: The new view to focus or not.
hasFocus: true if the view should be focused.", + "onViewChange": "Callback fired on view change.

Signature:
function(view: TView) => void
view: The new view.", + "openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from views list.", + "readOnly": "If true, the picker views and text field are read-only.", + "shouldDisableClock": "Disable specific clock time.

Signature:
function(clockValue: number, view: TimeView) => boolean
clockValue: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", + "shouldDisableTime": "Disable specific time.

Signature:
function(value: TDate, view: TimeView) => boolean
value: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", + "skipDisabled": "If true, disabled digital clock items will not be rendered.", + "slotProps": "The props used for each component slot.", + "slots": "Overrideable component slots.", + "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timeStep": "The time steps between two time options. For example, if timeStep = 45, then the available time options will be [00:00, 00:45, 01:30, 02:15, 03:00, etc.].", + "value": "The selected value. Used when the component is controlled.", + "view": "The visible view. Used when the component view is controlled. Must be a valid option from views list.", + "views": "Available views." + }, + "classDescriptions": { + "root": { "description": "Styles applied to the root element." }, + "list": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the list (by default: MenuList) element" + }, + "item": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the list item (by default: MenuItem) element" + } + }, + "slotDescriptions": { + "DigitalClockItem": "Component responsible for rendering a single digital clock item." + } +} diff --git a/docs/translations/api-docs/date-pickers/multi-section-digital-clock.json b/docs/translations/api-docs/date-pickers/multi-section-digital-clock.json new file mode 100644 index 000000000000..2f2f489d0f55 --- /dev/null +++ b/docs/translations/api-docs/date-pickers/multi-section-digital-clock.json @@ -0,0 +1,38 @@ +{ + "componentDescription": "", + "propDescriptions": { + "ampm": "12h/24h view for hour selection clock.", + "autoFocus": "If true, the main element is focused during the first mount. This main element is: - the element chosen by the visible view if any (i.e: the selected day on the day view). - the input element if there is a field rendered.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "components": "Overrideable components.", + "componentsProps": "The props used for each component slot.", + "defaultValue": "The default selected value. Used when the component is not controlled.", + "disabled": "If true, the picker views and text field are disabled.", + "disableFuture": "If true, disable values after the current date for date components, time for time components and both for date time components.", + "disableIgnoringDatePartForTimeValidation": "Do not ignore date part when validating min/max time.", + "disablePast": "If true, disable values before the current date for date components, time for time components and both for date time components.", + "focusedView": "Controlled focused view.", + "maxTime": "Maximal selectable time. The date part of the object will be ignored unless props.disableIgnoringDatePartForTimeValidation === true.", + "minTime": "Minimal selectable time. The date part of the object will be ignored unless props.disableIgnoringDatePartForTimeValidation === true.", + "minutesStep": "Step over minutes.", + "onChange": "Callback fired when the value changes.

Signature:
function(value: TDate | null, selectionState: PickerSelectionState | undefined, selectedView: TView | undefined) => void
value: The new value.
selectionState: Indicates if the date selection is complete.
selectedView: Indicates the view in which the selection has been made.", + "onFocusedViewChange": "Callback fired on focused view change.

Signature:
function(view: TView, hasFocus: boolean) => void
view: The new view to focus or not.
hasFocus: true if the view should be focused.", + "onViewChange": "Callback fired on view change.

Signature:
function(view: TView) => void
view: The new view.", + "openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from views list.", + "readOnly": "If true, the picker views and text field are read-only.", + "shouldDisableClock": "Disable specific clock time.

Signature:
function(clockValue: number, view: TimeView) => boolean
clockValue: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", + "shouldDisableTime": "Disable specific time.

Signature:
function(value: TDate, view: TimeView) => boolean
value: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", + "skipDisabled": "If true, disabled digital clock items will not be rendered.", + "slotProps": "The props used for each component slot.", + "slots": "Overrideable component slots.", + "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timeSteps": "The time steps between two time unit options. For example, if timeStep.minutes = 8, then the available minute options will be [0, 8, 16, 24, 32, 40, 48, 56].", + "value": "The selected value. Used when the component is controlled.", + "view": "The visible view. Used when the component view is controlled. Must be a valid option from views list.", + "views": "Available views." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } }, + "slotDescriptions": { + "DigitalClockSectionItem": "Component responsible for rendering a single multi section digital clock section item." + } +} diff --git a/docs/translations/api-docs/date-pickers/time-clock.json b/docs/translations/api-docs/date-pickers/time-clock.json index d71d77d5007f..9b515d6633f1 100644 --- a/docs/translations/api-docs/date-pickers/time-clock.json +++ b/docs/translations/api-docs/date-pickers/time-clock.json @@ -3,30 +3,32 @@ "propDescriptions": { "ampm": "12h/24h view for hour selection clock.", "ampmInClock": "Display ampm controls under the clock (instead of in the toolbar).", - "autoFocus": "Set to true if focus should be moved to clock picker.", + "autoFocus": "If true, the main element is focused during the first mount. This main element is: - the element chosen by the visible view if any (i.e: the selected day on the day view). - the input element if there is a field rendered.", "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", "components": "Overridable components.", "componentsProps": "The props used for each component slot.", "defaultValue": "The default selected value. Used when the component is not controlled.", - "disabled": "If true, the picker and text field are disabled.", + "disabled": "If true, the picker views and text field are disabled.", "disableFuture": "If true, disable values after the current date for date components, time for time components and both for date time components.", "disableIgnoringDatePartForTimeValidation": "Do not ignore date part when validating min/max time.", "disablePast": "If true, disable values before the current date for date components, time for time components and both for date time components.", + "focusedView": "Controlled focused view.", "maxTime": "Maximal selectable time. The date part of the object will be ignored unless props.disableIgnoringDatePartForTimeValidation === true.", "minTime": "Minimal selectable time. The date part of the object will be ignored unless props.disableIgnoringDatePartForTimeValidation === true.", "minutesStep": "Step over minutes.", - "onChange": "Callback fired when the value changes.

Signature:
function(value: TDate | null, selectionState: PickerSelectionState | undefined) => void
value: The new value.
selectionState: Indicates if the date selection is complete.", - "onViewChange": "Callback fired on view change.

Signature:
function(view: TimeView) => void
view: The new view.", - "openTo": "Initially open view.", - "readOnly": "Make picker read only.", + "onChange": "Callback fired when the value changes.

Signature:
function(value: TDate | null, selectionState: PickerSelectionState | undefined, selectedView: TView | undefined) => void
value: The new value.
selectionState: Indicates if the date selection is complete.
selectedView: Indicates the view in which the selection has been made.", + "onFocusedViewChange": "Callback fired on focused view change.

Signature:
function(view: TView, hasFocus: boolean) => void
view: The new view to focus or not.
hasFocus: true if the view should be focused.", + "onViewChange": "Callback fired on view change.

Signature:
function(view: TView) => void
view: The new view.", + "openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from views list.", + "readOnly": "If true, the picker views and text field are read-only.", "shouldDisableClock": "Disable specific clock time.

Signature:
function(clockValue: number, view: TimeView) => boolean
clockValue: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "shouldDisableTime": "Disable specific time.

Signature:
function(value: TDate, view: TimeView) => boolean
value: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "slotProps": "The props used for each component slot.", "slots": "Overridable component slots.", "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", "value": "The selected value. Used when the component is controlled.", - "view": "Controlled open view.", - "views": "Views for calendar picker." + "view": "The visible view. Used when the component view is controlled. Must be a valid option from views list.", + "views": "Available views." }, "classDescriptions": { "root": { "description": "Styles applied to the root element." }, diff --git a/docs/translations/api-docs/date-pickers/time-picker.json b/docs/translations/api-docs/date-pickers/time-picker.json index 4c5301a182c8..33cfe136b14d 100644 --- a/docs/translations/api-docs/date-pickers/time-picker.json +++ b/docs/translations/api-docs/date-pickers/time-picker.json @@ -36,9 +36,12 @@ "selectedSections": "The currently selected sections. This prop accept four formats: 1. If a number is provided, the section at this index will be selected. 2. If an object with a startIndex and endIndex properties are provided, the sections between those two indexes will be selected. 3. If a string of type FieldSectionType is provided, the first section with that name will be selected. 4. If null is provided, no section will be selected If not provided, the selected sections will be handled internally.", "shouldDisableClock": "Disable specific clock time.

Signature:
function(clockValue: number, view: TimeView) => boolean
clockValue: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "shouldDisableTime": "Disable specific time.

Signature:
function(value: TDate, view: TimeView) => boolean
value: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", + "skipDisabled": "If true, disabled digital clock items will not be rendered.", "slotProps": "The props used for each component slot.", "slots": "Overridable component slots.", "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "thresholdToRenderTimeInASingleColumn": "Amount of time options below or at which the single column time renderer is used.", + "timeSteps": "The time steps between two time unit options. For example, if timeStep.minutes = 8, then the available minute options will be [0, 8, 16, 24, 32, 40, 48, 56]. When single column time renderer is used, only timeStep.minutes will be used.", "value": "The selected value. Used when the component is controlled.", "view": "The visible view. Used when the component view is controlled. Must be a valid option from views list.", "viewRenderers": "Define custom view renderers for each section. If null, the section will only have field editing. If undefined, internally defined view will be the used.", @@ -51,6 +54,8 @@ "DesktopTransition": "Custom component for the desktop popper Transition.", "DesktopTrapFocus": "Custom component for trapping the focus inside the views on desktop.", "Dialog": "Custom component for the dialog inside which the views are rendered on mobile.", + "DigitalClockItem": "Component responsible for rendering a single digital clock item.", + "DigitalClockSectionItem": "Component responsible for rendering a single multi section digital clock section item.", "Field": "Component used to enter the date with the keyboard.", "InputAdornment": "Component displayed on the start or end input adornment used to open the picker on desktop.", "Layout": "Custom component for wrapping the layout.\nIt wraps the toolbar, views, action bar, and shortcuts.", diff --git a/packages/x-date-pickers-pro/src/dateRangeViewRenderers/dateRangeViewRenderers.tsx b/packages/x-date-pickers-pro/src/dateRangeViewRenderers/dateRangeViewRenderers.tsx index ab6cb70021d1..d22eaf891978 100644 --- a/packages/x-date-pickers-pro/src/dateRangeViewRenderers/dateRangeViewRenderers.tsx +++ b/packages/x-date-pickers-pro/src/dateRangeViewRenderers/dateRangeViewRenderers.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { DateOrTimeView } from '@mui/x-date-pickers/models'; +import { DateOrTimeViewWithMeridiem } from '@mui/x-date-pickers/internals/models'; import { DateRangeCalendar, DateRangeCalendarProps } from '../DateRangeCalendar'; -export interface DateRangeViewRendererProps +export interface DateRangeViewRendererProps extends DateRangeCalendarProps { view: TView; onViewChange?: (view: TView) => void; diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx b/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx index c7eb8d8c6402..3186163a2cbe 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx @@ -15,7 +15,7 @@ import { ExportedBaseToolbarProps, BaseFieldProps, } from '@mui/x-date-pickers/internals'; -import { DateOrTimeView } from '@mui/x-date-pickers/models'; +import { DateOrTimeViewWithMeridiem } from '@mui/x-date-pickers/internals/models'; import { DesktopRangePickerAdditionalViewProps, UseDesktopRangePickerParams, @@ -31,7 +31,7 @@ const releaseInfo = getReleaseInfo(); export const useDesktopRangePicker = < TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseDesktopRangePickerProps, >({ props, diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.types.ts b/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.types.ts index 02c30eb94e81..1147492a4b67 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.types.ts @@ -10,7 +10,7 @@ import { UsePickerValueNonStaticProps, UsePickerViewsNonStaticProps, } from '@mui/x-date-pickers/internals'; -import { DateOrTimeView } from '@mui/x-date-pickers/models'; +import { DateOrTimeViewWithMeridiem } from '@mui/x-date-pickers/internals/models'; import { ExportedPickersLayoutSlotsComponent, ExportedPickersLayoutSlotsComponentsProps, @@ -22,13 +22,17 @@ import { RangePickerFieldSlotsComponentsProps, } from '../useEnrichedRangePickerFieldProps'; -export interface UseDesktopRangePickerSlotsComponent - extends PickersPopperSlotsComponent, +export interface UseDesktopRangePickerSlotsComponent< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends PickersPopperSlotsComponent, ExportedPickersLayoutSlotsComponent, TDate, TView>, RangePickerFieldSlotsComponent {} -export interface UseDesktopRangePickerSlotsComponentsProps - extends PickersPopperSlotsComponentsProps, +export interface UseDesktopRangePickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends PickersPopperSlotsComponentsProps, ExportedPickersLayoutSlotsComponentsProps, TDate, TView>, RangePickerFieldSlotsComponentsProps { toolbar?: ExportedBaseToolbarProps; @@ -48,7 +52,7 @@ export interface DesktopRangeOnlyPickerProps export interface UseDesktopRangePickerProps< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, TExternalProps extends UsePickerViewsProps, > extends DesktopRangeOnlyPickerProps, @@ -77,7 +81,7 @@ export interface DesktopRangePickerAdditionalViewProps export interface UseDesktopRangePickerParams< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseDesktopRangePickerProps, > extends Pick< UsePickerParams< diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useEnrichedRangePickerFieldProps.ts b/packages/x-date-pickers-pro/src/internal/hooks/useEnrichedRangePickerFieldProps.ts index a0545f5dbd40..08ee993b34f2 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useEnrichedRangePickerFieldProps.ts +++ b/packages/x-date-pickers-pro/src/internal/hooks/useEnrichedRangePickerFieldProps.ts @@ -5,11 +5,8 @@ import TextField, { TextFieldProps } from '@mui/material/TextField'; import { resolveComponentProps, SlotComponentProps } from '@mui/base/utils'; import useEventCallback from '@mui/utils/useEventCallback'; import useForkRef from '@mui/utils/useForkRef'; -import { - BaseSingleInputFieldProps, - FieldSelectedSections, - DateOrTimeView, -} from '@mui/x-date-pickers/models'; +import { BaseSingleInputFieldProps, FieldSelectedSections } from '@mui/x-date-pickers/models'; +import { DateOrTimeViewWithMeridiem } from '@mui/x-date-pickers/internals/models'; import { PickersInputLocaleText } from '@mui/x-date-pickers/locales'; import { BaseFieldProps, @@ -71,7 +68,7 @@ export interface RangePickerFieldSlotsComponentsProps { export interface UseEnrichedRangePickerFieldPropsParams< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, FieldProps extends BaseFieldProps, RangeFieldSection, TError> = BaseFieldProps< DateRange, @@ -97,7 +94,7 @@ export interface UseEnrichedRangePickerFieldPropsParams< fieldProps: FieldProps; } -const useMultiInputFieldSlotProps = ({ +const useMultiInputFieldSlotProps = ({ wrapperVariant, open, actions, @@ -230,7 +227,7 @@ const useMultiInputFieldSlotProps = ({ +const useSingleInputFieldSlotProps = ({ wrapperVariant, open, actions, @@ -328,7 +325,11 @@ const useSingleInputFieldSlotProps = ( +export const useEnrichedRangePickerFieldProps = < + TDate, + TView extends DateOrTimeViewWithMeridiem, + TError, +>( params: UseEnrichedRangePickerFieldPropsParams, ) => { /* eslint-disable react-hooks/rules-of-hooks */ diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx b/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx index 2b12784b7c9f..33abbf9de8b6 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx @@ -13,8 +13,8 @@ import { ExportedBaseToolbarProps, useLocaleText, } from '@mui/x-date-pickers/internals'; -import { DateOrTimeView } from '@mui/x-date-pickers/models'; import useId from '@mui/utils/useId'; +import { DateOrTimeViewWithMeridiem } from '@mui/x-date-pickers/internals/models'; import { MobileRangePickerAdditionalViewProps, UseMobileRangePickerParams, @@ -30,7 +30,7 @@ const releaseInfo = getReleaseInfo(); export const useMobileRangePicker = < TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseMobileRangePickerProps, >({ props, diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.types.ts b/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.types.ts index c4542d54b072..2ba30b71723c 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.types.ts @@ -10,11 +10,11 @@ import { UsePickerValueNonStaticProps, UsePickerViewsNonStaticProps, } from '@mui/x-date-pickers/internals'; -import { DateOrTimeView } from '@mui/x-date-pickers/models'; import { ExportedPickersLayoutSlotsComponent, ExportedPickersLayoutSlotsComponentsProps, } from '@mui/x-date-pickers/PickersLayout'; +import { DateOrTimeViewWithMeridiem } from '@mui/x-date-pickers/internals/models'; import { DateRange, RangeFieldSection, BaseRangeNonStaticPickerProps } from '../../models'; import { UseRangePositionProps, UseRangePositionResponse } from '../useRangePosition'; import { @@ -22,13 +22,15 @@ import { RangePickerFieldSlotsComponentsProps, } from '../useEnrichedRangePickerFieldProps'; -export interface UseMobileRangePickerSlotsComponent +export interface UseMobileRangePickerSlotsComponent extends PickersModalDialogSlotsComponent, ExportedPickersLayoutSlotsComponent, TDate, TView>, RangePickerFieldSlotsComponent {} -export interface UseMobileRangePickerSlotsComponentsProps - extends PickersModalDialogSlotsComponentsProps, +export interface UseMobileRangePickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends PickersModalDialogSlotsComponentsProps, ExportedPickersLayoutSlotsComponentsProps, TDate, TView>, RangePickerFieldSlotsComponentsProps { toolbar?: ExportedBaseToolbarProps; @@ -43,7 +45,7 @@ export interface MobileRangeOnlyPickerProps export interface UseMobileRangePickerProps< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, TExternalProps extends UsePickerViewsProps, > extends MobileRangeOnlyPickerProps, @@ -72,7 +74,7 @@ export interface MobileRangePickerAdditionalViewProps export interface UseMobileRangePickerParams< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseMobileRangePickerProps, > extends Pick< UsePickerParams< diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useStaticRangePicker/useStaticRangePicker.tsx b/packages/x-date-pickers-pro/src/internal/hooks/useStaticRangePicker/useStaticRangePicker.tsx index 7dbfe7f00a8a..a1a97ab8aa6f 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useStaticRangePicker/useStaticRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internal/hooks/useStaticRangePicker/useStaticRangePicker.tsx @@ -7,7 +7,7 @@ import { PickersLayoutSlotsComponentsProps, } from '@mui/x-date-pickers/PickersLayout'; import { usePicker, DIALOG_WIDTH, ExportedBaseToolbarProps } from '@mui/x-date-pickers/internals'; -import { DateOrTimeView } from '@mui/x-date-pickers/models'; +import { DateOrTimeViewWithMeridiem } from '@mui/x-date-pickers/internals/models'; import { UseStaticRangePickerParams, UseStaticRangePickerProps, @@ -28,7 +28,7 @@ const PickerStaticLayout = styled(PickersLayout)(({ theme }) => ({ */ export const useStaticRangePicker = < TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseStaticRangePickerProps, >({ props, diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useStaticRangePicker/useStaticRangePicker.types.ts b/packages/x-date-pickers-pro/src/internal/hooks/useStaticRangePicker/useStaticRangePicker.types.ts index 7b249c48218b..c68398447ffd 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useStaticRangePicker/useStaticRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/internal/hooks/useStaticRangePicker/useStaticRangePicker.types.ts @@ -10,16 +10,18 @@ import { ExportedPickersLayoutSlotsComponent, ExportedPickersLayoutSlotsComponentsProps, } from '@mui/x-date-pickers/PickersLayout'; -import { DateOrTimeView } from '@mui/x-date-pickers/models'; +import { DateOrTimeViewWithMeridiem } from '@mui/x-date-pickers/internals/models'; import { DateRange } from '../../models/range'; import { UseRangePositionProps } from '../useRangePosition'; import { RangeFieldSection } from '../../models/fields'; -export interface UseStaticRangePickerSlotsComponent +export interface UseStaticRangePickerSlotsComponent extends ExportedPickersLayoutSlotsComponent, TDate, TView> {} -export interface UseStaticRangePickerSlotsComponentsProps - extends ExportedPickersLayoutSlotsComponentsProps, TDate, TView> { +export interface UseStaticRangePickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends ExportedPickersLayoutSlotsComponentsProps, TDate, TView> { toolbar?: ExportedBaseToolbarProps; } @@ -27,7 +29,7 @@ export interface StaticRangeOnlyPickerProps extends StaticOnlyPickerProps, UseRa export interface UseStaticRangePickerProps< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, TExternalProps extends UseStaticRangePickerProps, > extends BasePickerProps, TDate, TView, TError, TExternalProps, {}>, @@ -46,7 +48,7 @@ export interface UseStaticRangePickerProps< export interface UseStaticRangePickerParams< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseStaticRangePickerProps, > extends Pick< UsePickerParams, TDate, TView, RangeFieldSection, TExternalProps, {}>, diff --git a/packages/x-date-pickers/src/DatePicker/DatePickerToolbar.tsx b/packages/x-date-pickers/src/DatePicker/DatePickerToolbar.tsx index 5e8d48f375d3..60f46510cf69 100644 --- a/packages/x-date-pickers/src/DatePicker/DatePickerToolbar.tsx +++ b/packages/x-date-pickers/src/DatePicker/DatePickerToolbar.tsx @@ -164,9 +164,7 @@ DatePickerToolbar.propTypes = { * Currently visible picker view. */ view: PropTypes.oneOf(['day', 'month', 'year']).isRequired, - views: PropTypes.arrayOf( - PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']).isRequired, - ).isRequired, + views: PropTypes.arrayOf(PropTypes.oneOf(['day', 'month', 'year']).isRequired).isRequired, } as any; export { DatePickerToolbar }; diff --git a/packages/x-date-pickers/src/DateTimePicker/shared.tsx b/packages/x-date-pickers/src/DateTimePicker/shared.tsx index 51c8065f3c2e..12f8a4b7f2e3 100644 --- a/packages/x-date-pickers/src/DateTimePicker/shared.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/shared.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useThemeProps } from '@mui/material/styles'; import { DefaultizedProps } from '../internals/models/helpers'; -import { DateOrTimeView, DateTimeValidationError } from '../models'; +import { DateOrTimeView, DateTimeValidationError, TimeView } from '../models'; import { useDefaultDates, useUtils } from '../internals/hooks/useUtils'; import { DateCalendarSlotsComponent, @@ -11,7 +11,6 @@ import { import { TimeClockSlotsComponent, TimeClockSlotsComponentsProps, - ExportedTimeClockProps, } from '../TimeClock/TimeClock.types'; import { BasePickerInputProps } from '../internals/models/props/basePickerProps'; import { applyDefaultDate } from '../internals/utils/date-utils'; @@ -32,6 +31,7 @@ import { DateViewRendererProps } from '../dateViewRenderers'; import { TimeViewRendererProps } from '../timeViewRenderers'; import { applyDefaultViewProps } from '../internals/utils/views'; import { uncapitalizeObjectKeys, UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { BaseClockProps, ExportedBaseClockProps } from '../internals/models/props/clock'; export interface BaseDateTimePickerSlotsComponent extends DateCalendarSlotsComponent, @@ -64,7 +64,7 @@ export interface BaseDateTimePickerSlotsComponentsProps export interface BaseDateTimePickerProps extends BasePickerInputProps, Omit, 'onViewChange'>, - ExportedTimeClockProps { + ExportedBaseClockProps { /** * Display ampm controls under the clock (instead of in the toolbar). * @default true on desktop, false on mobile @@ -109,7 +109,8 @@ export interface BaseDateTimePickerProps PickerViewRendererLookup< TDate | null, DateOrTimeView, - DateViewRendererProps & TimeViewRendererProps, + DateViewRendererProps & + TimeViewRendererProps>, {} > >; diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx index 3c39f14f8c2b..13615e9f2428 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx @@ -6,11 +6,16 @@ import { TimeField } from '../TimeField'; import { DesktopTimePickerProps } from './DesktopTimePicker.types'; import { useTimePickerDefaultizedProps } from '../TimePicker/shared'; import { useLocaleText, validateTime } from '../internals'; -import { TimeView } from '../models'; import { Clock } from '../internals/components/icons'; import { useDesktopPicker } from '../internals/hooks/useDesktopPicker'; import { extractValidationProps } from '../internals/utils/validation/extractValidationProps'; import { PickerViewRendererLookup } from '../internals/hooks/usePicker/usePickerViews'; +import { + renderDigitalClockTimeView, + renderMultiSectionDigitalClockTimeView, +} from '../timeViewRenderers'; +import { PickersActionBarAction } from '../PickersActionBar'; +import { TimeViewWithMeridiem } from '../internals/models'; type DesktopTimePickerComponent = (( props: DesktopTimePickerProps & React.RefAttributes, @@ -23,25 +28,47 @@ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker( const localeText = useLocaleText(); // Props with the default values common to all time pickers - const defaultizedProps = useTimePickerDefaultizedProps>( - inProps, - 'MuiDesktopTimePicker', - ); + const defaultizedProps = useTimePickerDefaultizedProps< + TDate, + TimeViewWithMeridiem, + DesktopTimePickerProps + >(inProps, 'MuiDesktopTimePicker'); - const viewRenderers: PickerViewRendererLookup = { - hours: null, - minutes: null, - seconds: null, + const thresholdToRenderTimeInASingleColumn = + defaultizedProps.thresholdToRenderTimeInASingleColumn ?? 24; + const timeSteps = { hours: 1, minutes: 5, seconds: 5, ...defaultizedProps.timeSteps }; + const shouldRenderTimeInASingleColumn = + (24 * 60) / (timeSteps.hours * timeSteps.minutes) <= thresholdToRenderTimeInASingleColumn; + + const renderTimeView = shouldRenderTimeInASingleColumn + ? renderDigitalClockTimeView + : renderMultiSectionDigitalClockTimeView; + + const viewRenderers: PickerViewRendererLookup = { + hours: renderTimeView, + minutes: renderTimeView, + seconds: renderTimeView, + meridiem: renderTimeView, ...defaultizedProps.viewRenderers, }; const ampmInClock = defaultizedProps.ampmInClock ?? true; + const actionBarActions: PickersActionBarAction[] = shouldRenderTimeInASingleColumn + ? [] + : ['accept']; + const views: readonly TimeViewWithMeridiem[] = defaultizedProps.ampm + ? [...defaultizedProps.views, 'meridiem'] + : defaultizedProps.views; // Props with the default values specific to the desktop variant const props = { ...defaultizedProps, ampmInClock, + timeSteps, viewRenderers, + // Setting only `hours` time view in case of single column time picker + // Allows for easy view lifecycle management + views: shouldRenderTimeInASingleColumn ? ['hours' as TimeViewWithMeridiem] : views, slots: { field: TimeField, openPickerIcon: Clock, @@ -60,10 +87,14 @@ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker( ampmInClock, ...defaultizedProps.slotProps?.toolbar, }, + actionBar: { + actions: actionBarActions, + ...defaultizedProps.slotProps?.actionBar, + }, }, }; - const { renderPicker } = useDesktopPicker({ + const { renderPicker } = useDesktopPicker({ props, valueManager: singleItemValueManager, valueType: 'time', @@ -246,7 +277,7 @@ DesktopTimePicker.propTypes = { * Used when the component view is not controlled. * Must be a valid option from `views` list. */ - openTo: PropTypes.oneOf(['hours', 'minutes', 'seconds']), + openTo: PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']), /** * Force rendering in particular orientation. */ @@ -295,6 +326,11 @@ DesktopTimePicker.propTypes = { * @returns {boolean} If `true` the time will be disabled. */ shouldDisableTime: PropTypes.func, + /** + * If `true`, disabled digital clock items will not be rendered. + * @default false + */ + skipDisabled: PropTypes.bool, /** * The props used for each component slot. * @default {} @@ -313,6 +349,22 @@ DesktopTimePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Amount of time options below or at which the single column time renderer is used. + * @default 24 + */ + thresholdToRenderTimeInASingleColumn: PropTypes.number, + /** + * The time steps between two time unit options. + * For example, if `timeStep.minutes = 8`, then the available minute options will be `[0, 8, 16, 24, 32, 40, 48, 56]`. + * When single column time renderer is used, only `timeStep.minutes` will be used. + * @default{ hours: 1, minutes: 5, seconds: 5 } + */ + timeSteps: PropTypes.shape({ + hours: PropTypes.number, + minutes: PropTypes.number, + seconds: PropTypes.number, + }), /** * The selected value. * Used when the component is controlled. @@ -323,7 +375,7 @@ DesktopTimePicker.propTypes = { * Used when the component view is controlled. * Must be a valid option from `views` list. */ - view: PropTypes.oneOf(['hours', 'minutes', 'seconds']), + view: PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']), /** * Define custom view renderers for each section. * If `null`, the section will only have field editing. @@ -331,6 +383,7 @@ DesktopTimePicker.propTypes = { */ viewRenderers: PropTypes.shape({ hours: PropTypes.func, + meridiem: PropTypes.func, minutes: PropTypes.func, seconds: PropTypes.func, }), diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts index 618510e3aa45..38e9d03a7d40 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.types.ts @@ -9,20 +9,39 @@ import { BaseTimePickerSlotsComponentsProps, } from '../TimePicker/shared'; import { MakeOptional } from '../internals/models/helpers'; -import { TimeView } from '../models'; +import { TimeViewWithMeridiem } from '../internals/models'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { DesktopOnlyTimePickerProps } from '../internals/models/props/clock'; +import { DigitalClockSlotsComponent, DigitalClockSlotsComponentsProps } from '../DigitalClock'; +import { + MultiSectionDigitalClockSlotsComponent, + MultiSectionDigitalClockSlotsComponentsProps, +} from '../MultiSectionDigitalClock'; +import { TimeView } from '../models'; export interface DesktopTimePickerSlotsComponent extends BaseTimePickerSlotsComponent, - MakeOptional, 'Field' | 'OpenPickerIcon'> {} + MakeOptional< + UseDesktopPickerSlotsComponent, + 'Field' | 'OpenPickerIcon' + >, + DigitalClockSlotsComponent, + MultiSectionDigitalClockSlotsComponent {} export interface DesktopTimePickerSlotsComponentsProps extends BaseTimePickerSlotsComponentsProps, - ExportedUseDesktopPickerSlotsComponentsProps {} + ExportedUseDesktopPickerSlotsComponentsProps, + DigitalClockSlotsComponentsProps, + MultiSectionDigitalClockSlotsComponentsProps {} export interface DesktopTimePickerProps - extends BaseTimePickerProps, - DesktopOnlyPickerProps { + extends BaseTimePickerProps, + DesktopOnlyPickerProps, + DesktopOnlyTimePickerProps { + /** + * Available views. + */ + views?: readonly TimeView[]; /** * Overridable components. * @default {} diff --git a/packages/x-date-pickers/src/DesktopTimePicker/tests/DesktopTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopTimePicker/tests/DesktopTimePicker.test.tsx new file mode 100644 index 000000000000..4e7d81dcd1b7 --- /dev/null +++ b/packages/x-date-pickers/src/DesktopTimePicker/tests/DesktopTimePicker.test.tsx @@ -0,0 +1,160 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { screen, userEvent } from '@mui/monorepo/test/utils'; +import { DesktopTimePicker } from '@mui/x-date-pickers/DesktopTimePicker'; +import { createPickerRenderer, openPicker } from 'test/utils/pickers-utils'; + +describe('', () => { + const { render } = createPickerRenderer({ + clock: 'fake', + clockConfig: new Date('2018-01-01T10:05:05.000'), + }); + + describe('rendering behavior', () => { + it('should render "accept" action and 3 time sections by default', () => { + render(); + + expect(screen.getByRole('button', { name: 'OK' })).not.to.equal(null); + expect(screen.getByRole('listbox', { name: 'Select hours' })).not.to.equal(null); + expect(screen.getByRole('option', { name: '1 hours' })).not.to.equal(null); + expect(screen.getByRole('listbox', { name: 'Select minutes' })).not.to.equal(null); + expect(screen.getByRole('option', { name: '5 minutes' })).not.to.equal(null); + expect(screen.getByRole('listbox', { name: 'Select meridiem' })).not.to.equal(null); + expect(screen.getByRole('option', { name: 'AM' })).not.to.equal(null); + }); + + it('should render single column picker given big enough "thresholdToRenderTimeInASingleColumn" number', () => { + render(); + + expect(screen.getByRole('listbox', { name: 'Select time' })).not.to.equal(null); + expect(screen.getByRole('option', { name: '09:35 AM' })).not.to.equal(null); + }); + + it('should render single column picker given big enough "timeSteps.minutes" number', () => { + render(); + + expect(screen.getByRole('listbox', { name: 'Select time' })).not.to.equal(null); + expect(screen.getByRole('option', { name: '09:00 AM' })).not.to.equal(null); + }); + + it('should correctly use all "timeSteps"', () => { + render( + , + ); + + Array.from({ length: 12 / 3 }).forEach((_, i) => { + expect(screen.getByRole('option', { name: `${i * 3 || 12} hours` })).not.to.equal(null); + }); + Array.from({ length: 60 / 15 }).forEach((_, i) => { + expect(screen.getByRole('option', { name: `${i * 15} minutes` })).not.to.equal(null); + }); + Array.from({ length: 60 / 20 }).forEach((_, i) => { + expect(screen.getByRole('option', { name: `${i * 20} seconds` })).not.to.equal(null); + }); + }); + }); + + describe('selecting behavior', () => { + it('should call "onAccept", "onChange", and "onClose" when selecting a single option', () => { + const onChange = spy(); + const onAccept = spy(); + const onClose = spy(); + + render( + , + ); + + openPicker({ type: 'time', variant: 'desktop' }); + + userEvent.mousePress(screen.getByRole('option', { name: '09:00 AM' })); + expect(onChange.callCount).to.equal(1); + expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 1, 9, 0)); + expect(onAccept.callCount).to.equal(1); + expect(onAccept.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 1, 9, 0)); + expect(onClose.callCount).to.equal(1); + }); + + it('should call "onAccept", "onChange", and "onClose" when selecting all section', () => { + const onChange = spy(); + const onAccept = spy(); + const onClose = spy(); + + render(); + + openPicker({ type: 'time', variant: 'desktop' }); + + userEvent.mousePress(screen.getByRole('option', { name: '2 hours' })); + expect(onChange.callCount).to.equal(1); + expect(onAccept.callCount).to.equal(0); + expect(onClose.callCount).to.equal(0); + + userEvent.mousePress(screen.getByRole('option', { name: '15 minutes' })); + expect(onChange.callCount).to.equal(2); + expect(onAccept.callCount).to.equal(0); + expect(onClose.callCount).to.equal(0); + + userEvent.mousePress(screen.getByRole('option', { name: 'PM' })); + expect(onChange.callCount).to.equal(3); + expect(onAccept.callCount).to.equal(1); + expect(onAccept.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 1, 14, 15)); + expect(onClose.callCount).to.equal(1); + }); + + it('should allow out of order section selection', () => { + const onChange = spy(); + const onAccept = spy(); + const onClose = spy(); + + render(); + + openPicker({ type: 'time', variant: 'desktop' }); + + userEvent.mousePress(screen.getByRole('option', { name: '15 minutes' })); + expect(onChange.callCount).to.equal(1); + expect(onAccept.callCount).to.equal(0); + expect(onClose.callCount).to.equal(0); + + userEvent.mousePress(screen.getByRole('option', { name: '2 hours' })); + expect(onChange.callCount).to.equal(2); + expect(onAccept.callCount).to.equal(0); + expect(onClose.callCount).to.equal(0); + + userEvent.mousePress(screen.getByRole('option', { name: '25 minutes' })); + expect(onChange.callCount).to.equal(3); + expect(onAccept.callCount).to.equal(0); + expect(onClose.callCount).to.equal(0); + + userEvent.mousePress(screen.getByRole('option', { name: 'PM' })); + expect(onChange.callCount).to.equal(4); + expect(onAccept.callCount).to.equal(1); + expect(onAccept.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 1, 14, 25)); + expect(onClose.callCount).to.equal(1); + }); + + it('should finish selection when selecting only the last section', () => { + const onChange = spy(); + const onAccept = spy(); + const onClose = spy(); + + render(); + + openPicker({ type: 'time', variant: 'desktop' }); + + userEvent.mousePress(screen.getByRole('option', { name: 'PM' })); + expect(onChange.callCount).to.equal(1); + expect(onAccept.callCount).to.equal(1); + expect(onAccept.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 1, 12, 0)); + expect(onClose.callCount).to.equal(1); + }); + }); +}); diff --git a/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx index 87edb3d16464..9421131ada3f 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { userEvent, describeConformance } from '@mui/monorepo/test/utils'; +import { screen, userEvent, describeConformance } from '@mui/monorepo/test/utils'; import { describeValidation } from '@mui/x-date-pickers/tests/describeValidation'; import { describeValue } from '@mui/x-date-pickers/tests/describeValue'; import { @@ -23,7 +23,6 @@ describe(' - Describes', () => { render, fieldType: 'single-input', variant: 'desktop', - hasNoView: true, }); describeValidation(DesktopTimePicker, () => ({ @@ -58,6 +57,9 @@ describe(' - Describes', () => { componentFamily: 'picker', type: 'time', variant: 'desktop', + defaultProps: { + views: ['hours'], + }, values: [ adapterToUse.date(new Date(2018, 0, 1, 15, 30)), adapterToUse.date(new Date(2018, 0, 1, 18, 30)), @@ -77,17 +79,27 @@ describe(' - Describes', () => { : '', ); }, - setNewValue: (value, { isOpened } = {}) => { - const newValue = adapterToUse.addHours(value, 1); + setNewValue: (value, { isOpened, applySameValue } = {}) => { + const newValue = applySameValue ? value : adapterToUse.addHours(value, 1); if (isOpened) { - throw new Error("Can't test UI views on DesktopTimePicker"); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const hours = adapterToUse.format(newValue, hasMeridiem ? 'hours12h' : 'hours24h'); + const hoursNumber = adapterToUse.getHours(newValue); + userEvent.mousePress(screen.getByRole('option', { name: `${parseInt(hours, 10)} hours` })); + if (hasMeridiem) { + // meridiem is an extra view on `DesktopTimePicker` + // we need to click it to finish selection + userEvent.mousePress( + screen.getByRole('option', { name: hoursNumber >= 12 ? 'PM' : 'AM' }), + ); + } + } else { + const input = getTextbox(); + clickOnInput(input, 1); // Update the hour + userEvent.keyPress(input, { key: 'ArrowUp' }); } - const input = getTextbox(); - clickOnInput(input, 1); // Update the hour - userEvent.keyPress(input, { key: 'ArrowUp' }); - return newValue; }, })); diff --git a/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx b/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx new file mode 100644 index 000000000000..c5de1361cfc1 --- /dev/null +++ b/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx @@ -0,0 +1,464 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; +import { alpha, styled, useThemeProps } from '@mui/material/styles'; +import useEventCallback from '@mui/utils/useEventCallback'; +import composeClasses from '@mui/utils/composeClasses'; +import useControlled from '@mui/utils/useControlled'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import useForkRef from '@mui/utils/useForkRef'; +import { useUtils, useNow, useLocaleText } from '../internals/hooks/useUtils'; +import { createIsAfterIgnoreDatePart } from '../internals/utils/time-utils'; +import { PickerViewRoot } from '../internals/components/PickerViewRoot'; +import { getDigitalClockUtilityClass } from './digitalClockClasses'; +import { DigitalClockProps } from './DigitalClock.types'; +import { useViews } from '../internals/hooks/useViews'; +import { TimeView } from '../models'; +import { DIGITAL_CLOCK_VIEW_HEIGHT } from '../internals/constants/dimensions'; + +const useUtilityClasses = (ownerState: DigitalClockProps) => { + const { classes } = ownerState; + const slots = { + root: ['root'], + list: ['list'], + item: ['item'], + }; + + return composeClasses(slots, getDigitalClockUtilityClass, classes); +}; + +const DigitalClockRoot = styled(PickerViewRoot, { + name: 'MuiDigitalClock', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: DigitalClockProps & { alreadyRendered: boolean } }>(({ ownerState }) => ({ + overflowY: 'auto', + width: '100%', + scrollBehavior: ownerState.alreadyRendered ? 'smooth' : 'auto', + maxHeight: DIGITAL_CLOCK_VIEW_HEIGHT, +})); + +const DigitalClockList = styled(MenuList, { + name: 'MuiDigitalClock', + slot: 'List', + overridesResolver: (props, styles) => styles.list, +})({ + padding: 0, +}); + +const DigitalClockItem = styled(MenuItem, { + name: 'MuiDigitalClock', + slot: 'Item', + overridesResolver: (props, styles) => styles.item, +})(({ theme }) => ({ + padding: '8px 16px', + margin: '2px 4px', + '&:first-of-type': { + marginTop: 4, + }, + '&:hover': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity), + }, + '&.Mui-selected': { + backgroundColor: (theme.vars || theme).palette.primary.main, + color: (theme.vars || theme).palette.primary.contrastText, + '&:focus-visible, &:hover': { + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, + '&.Mui-focusVisible': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.focusOpacity})` + : alpha(theme.palette.primary.main, theme.palette.action.focusOpacity), + }, +})); + +type DigitalClockComponent = (( + props: DigitalClockProps & React.RefAttributes, +) => JSX.Element) & { propTypes?: any }; + +export const DigitalClock = React.forwardRef(function DigitalClock( + inProps: DigitalClockProps, + ref: React.Ref, +) { + const now = useNow(); + const utils = useUtils(); + const containerRef = React.useRef(null); + const handleRef = useForkRef(ref, containerRef); + const localeText = useLocaleText(); + + const props = useThemeProps({ + props: inProps, + name: 'MuiDigitalClock', + }); + + const { + ampm = utils.is12HourCycleInCurrentLocale(), + timeStep = 30, + autoFocus, + components, + componentsProps, + slots, + slotProps, + value: valueProp, + disableIgnoringDatePartForTimeValidation = false, + maxTime, + minTime, + disableFuture, + disablePast, + minutesStep = 1, + shouldDisableClock, + shouldDisableTime, + onChange, + defaultValue, + view: inView, + openTo, + onViewChange, + focusedView, + onFocusedViewChange, + className, + disabled, + readOnly, + views = ['hours'], + skipDisabled = false, + ...other + } = props; + + const ownerState = React.useMemo( + () => ({ ...props, alreadyRendered: !!containerRef.current }), + [props], + ); + + const classes = useUtilityClasses(ownerState); + + const ClockItem = slots?.digitalClockItem ?? components?.DigitalClockItem ?? DigitalClockItem; + const clockItemProps = slotProps?.digitalClockItem ?? componentsProps?.digitalClockItem; + + const [value, setValue] = useControlled({ + name: 'DigitalClock', + state: 'value', + controlled: valueProp, + default: defaultValue ?? null, + }); + + const handleValueChange = useEventCallback((newValue: TDate | null) => { + setValue(newValue); + onChange?.(newValue, 'finish'); + }); + + const { setValueAndGoToNextView } = useViews>({ + view: inView, + views, + openTo, + onViewChange, + onChange: handleValueChange, + focusedView, + onFocusedViewChange, + }); + + const handleItemSelect = useEventCallback((newValue: TDate | null) => { + setValueAndGoToNextView(newValue, 'finish'); + }); + + React.useEffect(() => { + if (containerRef.current === null) { + return; + } + const selectedItem = containerRef.current.querySelector( + '[role="listbox"] [role="option"][aria-selected="true"]', + ); + + if (!selectedItem) { + return; + } + const offsetTop = selectedItem.offsetTop; + + // Subtracting the 4px of extra margin intended for the first visible section item + containerRef.current.scrollTop = offsetTop - 4; + }); + + const selectedTimeOrMidnight = React.useMemo( + () => value || utils.setSeconds(utils.setMinutes(utils.setHours(now, 0), 0), 0), + [value, now, utils], + ); + + const isTimeDisabled = React.useCallback( + (valueToCheck: TDate) => { + const isAfter = createIsAfterIgnoreDatePart(disableIgnoringDatePartForTimeValidation, utils); + + const containsValidTime = () => { + if (minTime && isAfter(minTime, valueToCheck)) { + return false; + } + + if (maxTime && isAfter(valueToCheck, maxTime)) { + return false; + } + + if (disableFuture && isAfter(valueToCheck, now)) { + return false; + } + + if (disablePast && isAfter(now, valueToCheck)) { + return false; + } + + return true; + }; + + const isValidValue = () => { + if (utils.getMinutes(valueToCheck) % minutesStep !== 0) { + return false; + } + + if (shouldDisableClock?.(utils.toJsDate(valueToCheck).getTime(), 'hours')) { + return false; + } + + if (shouldDisableTime) { + return !shouldDisableTime(valueToCheck, 'hours'); + } + + return true; + }; + + return !containsValidTime() || !isValidValue(); + }, + [ + disableIgnoringDatePartForTimeValidation, + utils, + minTime, + maxTime, + disableFuture, + now, + disablePast, + minutesStep, + shouldDisableClock, + shouldDisableTime, + ], + ); + + const timeOptions = React.useMemo(() => { + const startOfDay = utils.startOfDay(selectedTimeOrMidnight); + return [ + startOfDay, + ...Array.from({ length: Math.ceil((24 * 60) / timeStep) - 1 }, (_, index) => + utils.addMinutes(startOfDay, timeStep * (index + 1)), + ), + utils.endOfDay(selectedTimeOrMidnight), + ]; + }, [selectedTimeOrMidnight, timeStep, utils]); + + return ( + + + {timeOptions.map((option) => { + if (skipDisabled && isTimeDisabled(option)) { + return null; + } + const isSelected = utils.isEqual(option, value); + return ( + !readOnly && handleItemSelect(option)} + selected={isSelected} + disabled={disabled || isTimeDisabled(option)} + disableRipple={readOnly} + role="option" + // aria-readonly is not supported here and does not have any effect + aria-disabled={readOnly} + aria-selected={isSelected} + {...clockItemProps} + > + {utils.format(option, ampm ? 'fullTime12h' : 'fullTime24h')} + + ); + })} + + + ); +}) as DigitalClockComponent; + +DigitalClock.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * 12h/24h view for hour selection clock. + * @default `utils.is12HourCycleInCurrentLocale()` + */ + ampm: PropTypes.bool, + /** + * If `true`, the main element is focused during the first mount. + * This main element is: + * - the element chosen by the visible view if any (i.e: the selected day on the `day` view). + * - the `input` element if there is a field rendered. + */ + autoFocus: PropTypes.bool, + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + className: PropTypes.string, + /** + * Overrideable components. + * @default {} + * @deprecated Please use `slots`. + */ + components: PropTypes.object, + /** + * The props used for each component slot. + * @default {} + * @deprecated Please use `slotProps`. + */ + componentsProps: PropTypes.object, + /** + * The default selected value. + * Used when the component is not controlled. + */ + defaultValue: PropTypes.any, + /** + * If `true`, the picker views and text field are disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * If `true`, disable values after the current date for date components, time for time components and both for date time components. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * If `true`, disable values before the current date for date components, time for time components and both for date time components. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Controlled focused view. + */ + focusedView: PropTypes.oneOf(['hours']), + /** + * Maximal selectable time. + * The date part of the object will be ignored unless `props.disableIgnoringDatePartForTimeValidation === true`. + */ + maxTime: PropTypes.any, + /** + * Minimal selectable time. + * The date part of the object will be ignored unless `props.disableIgnoringDatePartForTimeValidation === true`. + */ + minTime: PropTypes.any, + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * Callback fired when the value changes. + * @template TDate + * @param {TDate | null} value The new value. + * @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete. + * @param {TView | undefined} selectedView Indicates the view in which the selection has been made. + */ + onChange: PropTypes.func, + /** + * Callback fired on focused view change. + * @template TView + * @param {TView} view The new view to focus or not. + * @param {boolean} hasFocus `true` if the view should be focused. + */ + onFocusedViewChange: PropTypes.func, + /** + * Callback fired on view change. + * @template TView + * @param {TView} view The new view. + */ + onViewChange: PropTypes.func, + /** + * The default visible view. + * Used when the component view is not controlled. + * Must be a valid option from `views` list. + */ + openTo: PropTypes.oneOf(['hours']), + /** + * If `true`, the picker views and text field are read-only. + * @default false + */ + readOnly: PropTypes.bool, + /** + * Disable specific clock time. + * @param {number} clockValue The value to check. + * @param {TimeView} view The clock type of the timeValue. + * @returns {boolean} If `true` the time will be disabled. + * @deprecated Consider using `shouldDisableTime`. + */ + shouldDisableClock: PropTypes.func, + /** + * Disable specific time. + * @template TDate + * @param {TDate} value The value to check. + * @param {TimeView} view The clock type of the timeValue. + * @returns {boolean} If `true` the time will be disabled. + */ + shouldDisableTime: PropTypes.func, + /** + * If `true`, disabled digital clock items will not be rendered. + * @default false + */ + skipDisabled: PropTypes.bool, + /** + * The props used for each component slot. + * @default {} + */ + slotProps: PropTypes.object, + /** + * Overrideable component slots. + * @default {} + */ + slots: PropTypes.object, + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), + /** + * The time steps between two time options. + * For example, if `timeStep = 45`, then the available time options will be `[00:00, 00:45, 01:30, 02:15, 03:00, etc.]`. + * @default 30 + */ + timeStep: PropTypes.number, + /** + * The selected value. + * Used when the component is controlled. + */ + value: PropTypes.any, + /** + * The visible view. + * Used when the component view is controlled. + * Must be a valid option from `views` list. + */ + view: PropTypes.oneOf(['hours']), + /** + * Available views. + */ + views: PropTypes.arrayOf(PropTypes.oneOf(['hours'])), +} as any; diff --git a/packages/x-date-pickers/src/DigitalClock/DigitalClock.types.ts b/packages/x-date-pickers/src/DigitalClock/DigitalClock.types.ts new file mode 100644 index 000000000000..3d17c19a5cab --- /dev/null +++ b/packages/x-date-pickers/src/DigitalClock/DigitalClock.types.ts @@ -0,0 +1,57 @@ +import { SlotComponentProps } from '@mui/base/utils'; +import MenuItem from '@mui/material/MenuItem'; +import { DigitalClockClasses } from './digitalClockClasses'; +import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { + BaseClockProps, + DigitalClockOnlyProps, + ExportedBaseClockProps, +} from '../internals/models/props/clock'; +import { TimeView } from '../models'; + +export interface ExportedDigitalClockProps + extends ExportedBaseClockProps, + DigitalClockOnlyProps {} + +export interface DigitalClockSlotsComponent { + /** + * Component responsible for rendering a single digital clock item. + * @default MenuItem from '@mui/material' + */ + DigitalClockItem?: React.ElementType; +} + +export interface DigitalClockSlotsComponentsProps { + digitalClockItem?: SlotComponentProps>; +} + +export interface DigitalClockProps + extends ExportedDigitalClockProps, + BaseClockProps> { + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * Overrideable components. + * @default {} + * @deprecated Please use `slots`. + */ + components?: DigitalClockSlotsComponent; + /** + * The props used for each component slot. + * @default {} + * @deprecated Please use `slotProps`. + */ + componentsProps?: DigitalClockSlotsComponentsProps; + /** + * Overrideable component slots. + * @default {} + */ + slots?: UncapitalizeObjectKeys; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: DigitalClockSlotsComponentsProps; +} diff --git a/packages/x-date-pickers/src/DigitalClock/digitalClockClasses.ts b/packages/x-date-pickers/src/DigitalClock/digitalClockClasses.ts new file mode 100644 index 000000000000..a2bd879ae5c2 --- /dev/null +++ b/packages/x-date-pickers/src/DigitalClock/digitalClockClasses.ts @@ -0,0 +1,23 @@ +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; + +export interface DigitalClockClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the list (by default: MenuList) element. */ + list: string; + /** Styles applied to the list item (by default: MenuItem) element. */ + item: string; +} + +export type DigitalClockClassKey = keyof DigitalClockClasses; + +export function getDigitalClockUtilityClass(slot: string) { + return generateUtilityClass('MuiDigitalClock', slot); +} + +export const digitalClockClasses: DigitalClockClasses = generateUtilityClasses('MuiDigitalClock', [ + 'root', + 'list', + 'item', +]); diff --git a/packages/x-date-pickers/src/DigitalClock/index.ts b/packages/x-date-pickers/src/DigitalClock/index.ts new file mode 100644 index 000000000000..ccbe0d812c4c --- /dev/null +++ b/packages/x-date-pickers/src/DigitalClock/index.ts @@ -0,0 +1,10 @@ +export { DigitalClock } from './DigitalClock'; +export type { + DigitalClockProps, + DigitalClockSlotsComponent, + DigitalClockSlotsComponentsProps, + ExportedDigitalClockProps, +} from './DigitalClock.types'; + +export { digitalClockClasses, getDigitalClockUtilityClass } from './digitalClockClasses'; +export type { DigitalClockClasses, DigitalClockClassKey } from './digitalClockClasses'; diff --git a/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx b/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx new file mode 100644 index 000000000000..b3c358217efb --- /dev/null +++ b/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { screen, userEvent, describeConformance } from '@mui/monorepo/test/utils'; +import { describeValidation } from '@mui/x-date-pickers/tests/describeValidation'; +import { describeValue } from '@mui/x-date-pickers/tests/describeValue'; +import { createPickerRenderer, adapterToUse, wrapPickerMount } from 'test/utils/pickers-utils'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { expect } from 'chai'; + +describe(' - Describes', () => { + const { render, clock } = createPickerRenderer({ clock: 'fake' }); + + describeValidation(DigitalClock, () => ({ + render, + clock, + views: ['hours'], + componentFamily: 'digital-clock', + variant: 'desktop', + })); + + describeConformance(, () => ({ + classes: {} as any, + render, + muiName: 'MuiDigitalClock', + wrapMount: wrapPickerMount, + refInstanceof: window.HTMLDivElement, + skip: [ + 'componentProp', + 'componentsProp', + 'themeDefaultProps', + 'themeStyleOverrides', + 'themeVariants', + 'mergeClassName', + 'propsSpread', + 'rootClass', + 'reactTestRenderer', + ], + })); + + describeValue(DigitalClock, () => ({ + render, + componentFamily: 'digital-clock', + type: 'time', + variant: 'desktop', + defaultProps: { + views: ['hours'], + }, + values: [ + adapterToUse.date(new Date(2018, 0, 1, 15, 30)), + adapterToUse.date(new Date(2018, 0, 1, 17, 0)), + ], + emptyValue: null, + clock, + assertRenderedValue: (expectedValue: any) => { + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const selectedItem = screen.queryByRole('option', { selected: true }); + if (!expectedValue) { + expect(selectedItem).to.equal(null); + } else { + expect(selectedItem).to.have.text( + adapterToUse.format(expectedValue, hasMeridiem ? 'fullTime12h' : 'fullTime24h'), + ); + } + }, + setNewValue: (value) => { + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const formattedLabel = adapterToUse.format( + value, + hasMeridiem ? 'fullTime12h' : 'fullTime24h', + ); + userEvent.mousePress(screen.getByRole('option', { name: formattedLabel })); + + return value; + }, + })); +}); diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx index 802f55a6acc9..5635d34323ae 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx @@ -22,10 +22,11 @@ const MobileTimePicker = React.forwardRef(function MobileTimePicker( const localeText = useLocaleText(); // Props with the default values common to all time pickers - const defaultizedProps = useTimePickerDefaultizedProps>( - inProps, - 'MuiMobileTimePicker', - ); + const defaultizedProps = useTimePickerDefaultizedProps< + TDate, + TimeView, + MobileTimePickerProps + >(inProps, 'MuiMobileTimePicker'); const viewRenderers: PickerViewRendererLookup = { hours: renderTimeViewClock, diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts index a9802531c8dc..98412691d2f4 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.types.ts @@ -10,39 +10,44 @@ import { } from '../TimePicker/shared'; import { MakeOptional } from '../internals/models/helpers'; import { TimeView } from '../models'; +import { TimeViewWithMeridiem } from '../internals/models'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; -export interface MobileTimePickerSlotsComponent - extends BaseTimePickerSlotsComponent, - MakeOptional, 'Field'> {} +export interface MobileTimePickerSlotsComponent< + TDate, + TView extends TimeViewWithMeridiem = TimeView, +> extends BaseTimePickerSlotsComponent, + MakeOptional, 'Field'> {} -export interface MobileTimePickerSlotsComponentsProps - extends BaseTimePickerSlotsComponentsProps, - ExportedUseMobilePickerSlotsComponentsProps {} +export interface MobileTimePickerSlotsComponentsProps< + TDate, + TView extends TimeViewWithMeridiem = TimeView, +> extends BaseTimePickerSlotsComponentsProps, + ExportedUseMobilePickerSlotsComponentsProps {} -export interface MobileTimePickerProps - extends BaseTimePickerProps, +export interface MobileTimePickerProps + extends BaseTimePickerProps, MobileOnlyPickerProps { /** * Overridable components. * @default {} * @deprecated Please use `slots`. */ - components?: MobileTimePickerSlotsComponent; + components?: MobileTimePickerSlotsComponent; /** * The props used for each component slot. * @default {} * @deprecated Please use `slotProps`. */ - componentsProps?: MobileTimePickerSlotsComponentsProps; + componentsProps?: MobileTimePickerSlotsComponentsProps; /** * Overridable component slots. * @default {} */ - slots?: UncapitalizeObjectKeys>; + slots?: UncapitalizeObjectKeys>; /** * The props used for each component slot. * @default {} */ - slotProps?: MobileTimePickerSlotsComponentsProps; + slotProps?: MobileTimePickerSlotsComponentsProps; } diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx new file mode 100644 index 000000000000..6095ab3f1947 --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx @@ -0,0 +1,579 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; +import { styled, useThemeProps } from '@mui/material/styles'; +import useEventCallback from '@mui/utils/useEventCallback'; +import composeClasses from '@mui/utils/composeClasses'; +import useControlled from '@mui/utils/useControlled'; +import { useUtils, useNow, useLocaleText } from '../internals/hooks/useUtils'; +import { convertValueToMeridiem, createIsAfterIgnoreDatePart } from '../internals/utils/time-utils'; +import { useViews } from '../internals/hooks/useViews'; +import type { PickerSelectionState } from '../internals/hooks/usePicker'; +import { useMeridiemMode } from '../internals/hooks/date-helpers-hooks'; +import { PickerViewRoot } from '../internals/components/PickerViewRoot'; +import { getMultiSectionDigitalClockUtilityClass } from './multiSectionDigitalClockClasses'; +import { MultiSectionDigitalClockSection } from './MultiSectionDigitalClockSection'; +import { + MultiSectionDigitalClockProps, + MultiSectionDigitalClockViewProps, +} from './MultiSectionDigitalClock.types'; +import { getHourSectionOptions, getTimeSectionOptions } from './MultiSectionDigitalClock.utils'; +import { TimeStepOptions, TimeView } from '../models'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const useUtilityClasses = (ownerState: MultiSectionDigitalClockProps) => { + const { classes } = ownerState; + const slots = { + root: ['root'], + }; + + return composeClasses(slots, getMultiSectionDigitalClockUtilityClass, classes); +}; + +const MultiSectionDigitalClockRoot = styled(PickerViewRoot, { + name: 'MuiMultiSectionDigitalClock', + slot: 'Root', + overridesResolver: (_, styles) => styles.root, +})<{ ownerState: MultiSectionDigitalClockProps }>(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + width: '100%', + borderBottom: `1px solid ${(theme.vars || theme).palette.divider}`, +})); + +type MultiSectionDigitalClockComponent = (( + props: MultiSectionDigitalClockProps & React.RefAttributes, +) => JSX.Element) & { propTypes?: any }; + +export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDigitalClock< + TDate extends unknown, +>(inProps: MultiSectionDigitalClockProps, ref: React.Ref) { + const now = useNow(); + const utils = useUtils(); + const localeText = useLocaleText(); + + const props = useThemeProps({ + props: inProps, + name: 'MuiMultiSectionDigitalClock', + }); + + const { + ampm = utils.is12HourCycleInCurrentLocale(), + timeSteps: inTimeSteps, + autoFocus, + components, + componentsProps, + slots, + slotProps, + value: valueProp, + disableIgnoringDatePartForTimeValidation = false, + maxTime, + minTime, + disableFuture, + disablePast, + minutesStep = 1, + shouldDisableClock, + shouldDisableTime, + onChange, + defaultValue, + view: inView, + views: inViews = ['hours', 'minutes'], + openTo, + onViewChange, + focusedView: inFocusedView, + onFocusedViewChange, + className, + disabled, + readOnly, + skipDisabled = false, + ...other + } = props; + const timeSteps = React.useMemo>( + () => ({ + hours: 1, + minutes: 5, + seconds: 5, + ...inTimeSteps, + }), + [inTimeSteps], + ); + + const [value, setValue] = useControlled({ + name: 'MultiSectionDigitalClock', + state: 'value', + controlled: valueProp, + default: defaultValue ?? null, + }); + + const handleValueChange = useEventCallback( + ( + newValue: TDate | null, + selectionState?: PickerSelectionState, + selectedView?: TimeViewWithMeridiem, + ) => { + setValue(newValue); + onChange?.(newValue, selectionState, selectedView); + }, + ); + + const views = React.useMemo(() => { + if (!ampm || !inViews.includes('hours')) { + return inViews; + } + return inViews.includes('meridiem') ? inViews : [...inViews, 'meridiem']; + }, [ampm, inViews]); + + const { view, setValueAndGoToView, focusedView } = useViews({ + view: inView, + views, + openTo, + onViewChange, + onChange: handleValueChange, + focusedView: inFocusedView, + onFocusedViewChange, + }); + + const selectedTimeOrMidnight = React.useMemo( + () => value || utils.setSeconds(utils.setMinutes(utils.setHours(now, 0), 0), 0), + [value, now, utils], + ); + + const handleMeridiemValueChange = useEventCallback((newValue: TDate | null) => { + setValueAndGoToView(newValue, null, 'meridiem'); + }); + + const { meridiemMode, handleMeridiemChange } = useMeridiemMode( + selectedTimeOrMidnight, + ampm, + handleMeridiemValueChange, + 'finish', + ); + + const isTimeDisabled = React.useCallback( + (rawValue: number, viewType: TimeView) => { + const isAfter = createIsAfterIgnoreDatePart(disableIgnoringDatePartForTimeValidation, utils); + const shouldCheckPastEnd = + viewType === 'hours' || (viewType === 'minutes' && views.includes('seconds')); + + const containsValidTime = ({ start, end }: { start: TDate; end: TDate }) => { + if (minTime && isAfter(minTime, end)) { + return false; + } + + if (maxTime && isAfter(start, maxTime)) { + return false; + } + + if (disableFuture && isAfter(start, now)) { + return false; + } + + if (disablePast && isAfter(now, shouldCheckPastEnd ? end : start)) { + return false; + } + + return true; + }; + + const isValidValue = (timeValue: number, step = 1) => { + if (timeValue % step !== 0) { + return false; + } + + if (shouldDisableClock?.(timeValue, viewType)) { + return false; + } + + if (shouldDisableTime) { + switch (viewType) { + case 'hours': + return !shouldDisableTime(utils.setHours(selectedTimeOrMidnight, timeValue), 'hours'); + case 'minutes': + return !shouldDisableTime( + utils.setMinutes(selectedTimeOrMidnight, timeValue), + 'minutes', + ); + + case 'seconds': + return !shouldDisableTime( + utils.setSeconds(selectedTimeOrMidnight, timeValue), + 'seconds', + ); + + default: + return false; + } + } + + return true; + }; + + switch (viewType) { + case 'hours': { + const valueWithMeridiem = convertValueToMeridiem(rawValue, meridiemMode, ampm); + const dateWithNewHours = utils.setHours(selectedTimeOrMidnight, valueWithMeridiem); + const start = utils.setSeconds(utils.setMinutes(dateWithNewHours, 0), 0); + const end = utils.setSeconds(utils.setMinutes(dateWithNewHours, 59), 59); + + return !containsValidTime({ start, end }) || !isValidValue(valueWithMeridiem); + } + + case 'minutes': { + const dateWithNewMinutes = utils.setMinutes(selectedTimeOrMidnight, rawValue); + const start = utils.setSeconds(dateWithNewMinutes, 0); + const end = utils.setSeconds(dateWithNewMinutes, 59); + + return !containsValidTime({ start, end }) || !isValidValue(rawValue, minutesStep); + } + + case 'seconds': { + const dateWithNewSeconds = utils.setSeconds(selectedTimeOrMidnight, rawValue); + const start = dateWithNewSeconds; + const end = dateWithNewSeconds; + + return !containsValidTime({ start, end }) || !isValidValue(rawValue); + } + + default: + throw new Error('not supported'); + } + }, + [ + ampm, + selectedTimeOrMidnight, + disableIgnoringDatePartForTimeValidation, + maxTime, + meridiemMode, + minTime, + minutesStep, + shouldDisableClock, + shouldDisableTime, + utils, + disableFuture, + disablePast, + now, + views, + ], + ); + + const handleSectionChange = useEventCallback( + (sectionView: TimeViewWithMeridiem, newValue: TDate | null) => { + const viewIndex = views.indexOf(sectionView); + const nextView = views[viewIndex + 1]; + setValueAndGoToView(newValue, nextView, sectionView); + }, + ); + + const buildViewProps = React.useCallback( + (viewToBuild: TimeViewWithMeridiem): MultiSectionDigitalClockViewProps => { + switch (viewToBuild) { + case 'hours': { + return { + onChange: (hours) => { + const valueWithMeridiem = convertValueToMeridiem(hours, meridiemMode, ampm); + handleSectionChange( + 'hours', + utils.setHours(selectedTimeOrMidnight, valueWithMeridiem), + ); + }, + items: getHourSectionOptions({ + now, + value, + ampm, + utils, + isDisabled: (hours) => disabled || isTimeDisabled(hours, 'hours'), + timeStep: timeSteps.hours, + resolveAriaLabel: localeText.hoursClockNumberText, + }), + }; + } + + case 'minutes': { + return { + onChange: (minutes) => { + handleSectionChange('minutes', utils.setMinutes(selectedTimeOrMidnight, minutes)); + }, + items: getTimeSectionOptions({ + value: utils.getMinutes(selectedTimeOrMidnight), + isDisabled: (minutes) => disabled || isTimeDisabled(minutes, 'minutes'), + resolveLabel: (minutes) => utils.format(utils.setMinutes(now, minutes), 'minutes'), + timeStep: timeSteps.minutes, + hasValue: !!value, + resolveAriaLabel: localeText.minutesClockNumberText, + }), + }; + } + + case 'seconds': { + return { + onChange: (seconds) => { + handleSectionChange('seconds', utils.setSeconds(selectedTimeOrMidnight, seconds)); + }, + items: getTimeSectionOptions({ + value: utils.getSeconds(selectedTimeOrMidnight), + isDisabled: (seconds) => disabled || isTimeDisabled(seconds, 'seconds'), + resolveLabel: (seconds) => utils.format(utils.setSeconds(now, seconds), 'seconds'), + timeStep: timeSteps.seconds, + hasValue: !!value, + resolveAriaLabel: localeText.secondsClockNumberText, + }), + }; + } + + case 'meridiem': { + const amLabel = utils.getMeridiemText('am'); + const pmLabel = utils.getMeridiemText('pm'); + return { + onChange: handleMeridiemChange, + items: [ + { + value: 'am', + label: amLabel, + isSelected: () => !!value && meridiemMode === 'am', + ariaLabel: amLabel, + }, + { + value: 'pm', + label: pmLabel, + isSelected: () => !!value && meridiemMode === 'pm', + ariaLabel: pmLabel, + }, + ], + }; + } + + default: + throw new Error(`Unknown view: ${viewToBuild} found.`); + } + }, + [ + now, + value, + ampm, + utils, + timeSteps.hours, + timeSteps.minutes, + timeSteps.seconds, + localeText.hoursClockNumberText, + localeText.minutesClockNumberText, + localeText.secondsClockNumberText, + meridiemMode, + handleSectionChange, + selectedTimeOrMidnight, + disabled, + isTimeDisabled, + handleMeridiemChange, + ], + ); + + const viewTimeOptions = React.useMemo(() => { + return views.reduce((result, currentView) => { + return { ...result, [currentView]: buildViewProps(currentView) }; + }, {} as Record>); + }, [views, buildViewProps]); + + const ownerState = props; + const classes = useUtilityClasses(ownerState); + + return ( + + {Object.entries(viewTimeOptions).map(([timeView, viewOptions]) => ( + + ))} + + ); +}) as MultiSectionDigitalClockComponent; + +MultiSectionDigitalClock.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * 12h/24h view for hour selection clock. + * @default `utils.is12HourCycleInCurrentLocale()` + */ + ampm: PropTypes.bool, + /** + * If `true`, the main element is focused during the first mount. + * This main element is: + * - the element chosen by the visible view if any (i.e: the selected day on the `day` view). + * - the `input` element if there is a field rendered. + */ + autoFocus: PropTypes.bool, + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + className: PropTypes.string, + /** + * Overrideable components. + * @default {} + * @deprecated Please use `slots`. + */ + components: PropTypes.object, + /** + * The props used for each component slot. + * @default {} + * @deprecated Please use `slotProps`. + */ + componentsProps: PropTypes.object, + /** + * The default selected value. + * Used when the component is not controlled. + */ + defaultValue: PropTypes.any, + /** + * If `true`, the picker views and text field are disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * If `true`, disable values after the current date for date components, time for time components and both for date time components. + * @default false + */ + disableFuture: PropTypes.bool, + /** + * Do not ignore date part when validating min/max time. + * @default false + */ + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + /** + * If `true`, disable values before the current date for date components, time for time components and both for date time components. + * @default false + */ + disablePast: PropTypes.bool, + /** + * Controlled focused view. + */ + focusedView: PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']), + /** + * Maximal selectable time. + * The date part of the object will be ignored unless `props.disableIgnoringDatePartForTimeValidation === true`. + */ + maxTime: PropTypes.any, + /** + * Minimal selectable time. + * The date part of the object will be ignored unless `props.disableIgnoringDatePartForTimeValidation === true`. + */ + minTime: PropTypes.any, + /** + * Step over minutes. + * @default 1 + */ + minutesStep: PropTypes.number, + /** + * Callback fired when the value changes. + * @template TDate + * @param {TDate | null} value The new value. + * @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete. + * @param {TView | undefined} selectedView Indicates the view in which the selection has been made. + */ + onChange: PropTypes.func, + /** + * Callback fired on focused view change. + * @template TView + * @param {TView} view The new view to focus or not. + * @param {boolean} hasFocus `true` if the view should be focused. + */ + onFocusedViewChange: PropTypes.func, + /** + * Callback fired on view change. + * @template TView + * @param {TView} view The new view. + */ + onViewChange: PropTypes.func, + /** + * The default visible view. + * Used when the component view is not controlled. + * Must be a valid option from `views` list. + */ + openTo: PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']), + /** + * If `true`, the picker views and text field are read-only. + * @default false + */ + readOnly: PropTypes.bool, + /** + * Disable specific clock time. + * @param {number} clockValue The value to check. + * @param {TimeView} view The clock type of the timeValue. + * @returns {boolean} If `true` the time will be disabled. + * @deprecated Consider using `shouldDisableTime`. + */ + shouldDisableClock: PropTypes.func, + /** + * Disable specific time. + * @template TDate + * @param {TDate} value The value to check. + * @param {TimeView} view The clock type of the timeValue. + * @returns {boolean} If `true` the time will be disabled. + */ + shouldDisableTime: PropTypes.func, + /** + * If `true`, disabled digital clock items will not be rendered. + * @default false + */ + skipDisabled: PropTypes.bool, + /** + * The props used for each component slot. + * @default {} + */ + slotProps: PropTypes.object, + /** + * Overrideable component slots. + * @default {} + */ + slots: PropTypes.object, + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), + /** + * The time steps between two time unit options. + * For example, if `timeStep.minutes = 8`, then the available minute options will be `[0, 8, 16, 24, 32, 40, 48, 56]`. + * @default{ hours: 1, minutes: 5, seconds: 5 } + */ + timeSteps: PropTypes.shape({ + hours: PropTypes.number, + minutes: PropTypes.number, + seconds: PropTypes.number, + }), + /** + * The selected value. + * Used when the component is controlled. + */ + value: PropTypes.any, + /** + * The visible view. + * Used when the component view is controlled. + * Must be a valid option from `views` list. + */ + view: PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']), + /** + * Available views. + */ + views: PropTypes.arrayOf(PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']).isRequired), +} as any; diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.types.ts b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.types.ts new file mode 100644 index 000000000000..bc51ef50d49f --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.types.ts @@ -0,0 +1,69 @@ +import { SlotComponentProps } from '@mui/base/utils'; +import MenuItem from '@mui/material/MenuItem'; +import { MultiSectionDigitalClockClasses } from './multiSectionDigitalClockClasses'; +import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { + BaseClockProps, + ExportedBaseClockProps, + MultiSectionDigitalClockOnlyProps, +} from '../internals/models/props/clock'; +import { MultiSectionDigitalClockSectionProps } from './MultiSectionDigitalClockSection'; +import { TimeViewWithMeridiem } from '../internals/models'; + +export interface MultiSectionDigitalClockOption { + isDisabled?: (value: TValue) => boolean; + isSelected: (value: TValue) => boolean; + label: string; + value: TValue; + ariaLabel: string; +} + +export interface ExportedMultiSectionDigitalClockProps + extends ExportedBaseClockProps, + MultiSectionDigitalClockOnlyProps {} + +export interface MultiSectionDigitalClockViewProps + extends Pick, 'onChange' | 'items'> {} + +export interface MultiSectionDigitalClockSlotsComponent { + /** + * Component responsible for rendering a single multi section digital clock section item. + * @default MenuItem from '@mui/material' + */ + DigitalClockSectionItem?: React.ElementType; +} + +export interface MultiSectionDigitalClockSlotsComponentsProps { + digitalClockSectionItem?: SlotComponentProps>; +} + +export interface MultiSectionDigitalClockProps + extends ExportedMultiSectionDigitalClockProps, + BaseClockProps { + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * Overrideable components. + * @default {} + * @deprecated Please use `slots`. + */ + components?: MultiSectionDigitalClockSlotsComponent; + /** + * The props used for each component slot. + * @default {} + * @deprecated Please use `slotProps`. + */ + componentsProps?: MultiSectionDigitalClockSlotsComponentsProps; + /** + * Overrideable component slots. + * @default {} + */ + slots?: UncapitalizeObjectKeys; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: MultiSectionDigitalClockSlotsComponentsProps; +} diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.utils.ts b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.utils.ts new file mode 100644 index 000000000000..da44fac41954 --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.utils.ts @@ -0,0 +1,98 @@ +import { MuiPickersAdapter } from '../models'; +import { MultiSectionDigitalClockOption } from './MultiSectionDigitalClock.types'; + +interface IGetHoursSectionOptions { + now: TDate; + value: TDate | null; + utils: MuiPickersAdapter; + ampm: boolean; + isDisabled: (value: number) => boolean; + timeStep: number; + resolveAriaLabel: (value: string) => string; +} + +export const getHourSectionOptions = ({ + now, + value, + utils, + ampm, + isDisabled, + resolveAriaLabel, + timeStep, +}: IGetHoursSectionOptions): MultiSectionDigitalClockOption[] => { + const currentHours = value ? utils.getHours(value) : null; + + const result: MultiSectionDigitalClockOption[] = []; + + const isSelected = (hour: number) => { + if (currentHours === null) { + return false; + } + + if (ampm) { + if (hour === 12) { + return currentHours === 12 || currentHours === 0; + } + + return currentHours === hour || currentHours - 12 === hour; + } + + return currentHours === hour; + }; + + const endHour = ampm ? 11 : 23; + for (let hour = 0; hour <= endHour; hour += timeStep) { + let label = utils.format(utils.setHours(now, hour), ampm ? 'hours12h' : 'hours24h'); + const ariaLabel = resolveAriaLabel(parseInt(label, 10).toString()); + + label = utils.formatNumber(label); + + result.push({ + value: hour, + label, + isSelected, + isDisabled, + ariaLabel, + }); + } + return result; +}; + +interface IGetTimeSectionOptions { + value: number | null; + isDisabled: (value: number) => boolean; + timeStep: number; + resolveLabel: (value: number) => string; + hasValue?: boolean; + resolveAriaLabel: (value: string) => string; +} + +export const getTimeSectionOptions = ({ + value, + isDisabled, + timeStep, + resolveLabel, + resolveAriaLabel, + hasValue = true, +}: IGetTimeSectionOptions): MultiSectionDigitalClockOption[] => { + const isSelected = (timeValue: number) => { + if (value === null) { + return false; + } + + return hasValue && value === timeValue; + }; + + return [ + ...Array.from({ length: Math.ceil(60 / timeStep) }, (_, index) => { + const timeValue = timeStep * index; + return { + value: timeValue, + label: resolveLabel(timeValue), + isDisabled, + isSelected, + ariaLabel: resolveAriaLabel(timeValue.toString()), + }; + }), + ]; +}; diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx new file mode 100644 index 000000000000..f6b032929f39 --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx @@ -0,0 +1,201 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { alpha, styled, useThemeProps } from '@mui/material/styles'; +import composeClasses from '@mui/utils/composeClasses'; +import MenuList from '@mui/material/MenuList'; +import MenuItem from '@mui/material/MenuItem'; +import useForkRef from '@mui/utils/useForkRef'; +import { + MultiSectionDigitalClockSectionClasses, + getMultiSectionDigitalClockSectionUtilityClass, +} from './multiSectionDigitalClockSectionClasses'; +import type { + MultiSectionDigitalClockOption, + MultiSectionDigitalClockSlotsComponent, + MultiSectionDigitalClockSlotsComponentsProps, +} from './MultiSectionDigitalClock.types'; +import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { DIGITAL_CLOCK_VIEW_HEIGHT } from '../internals/constants/dimensions'; + +export interface ExportedMultiSectionDigitalClockSectionProps { + className?: string; + classes?: Partial; + slots?: UncapitalizeObjectKeys; + slotProps?: MultiSectionDigitalClockSlotsComponentsProps; +} + +export interface MultiSectionDigitalClockSectionProps + extends ExportedMultiSectionDigitalClockSectionProps { + autoFocus?: boolean; + disabled?: boolean; + readOnly?: boolean; + items: MultiSectionDigitalClockOption[]; + onChange: (value: TValue) => void; + active?: boolean; + skipDisabled?: boolean; + role?: string; +} + +const useUtilityClasses = (ownerState: MultiSectionDigitalClockSectionProps) => { + const { classes } = ownerState; + const slots = { + root: ['root'], + item: ['item'], + }; + + return composeClasses(slots, getMultiSectionDigitalClockSectionUtilityClass, classes); +}; + +const MultiSectionDigitalClockSectionRoot = styled(MenuList, { + name: 'MuiMultiSectionDigitalClockSection', + slot: 'Root', + overridesResolver: (_, styles) => styles.root, +})<{ ownerState: MultiSectionDigitalClockSectionProps & { alreadyRendered: boolean } }>( + ({ theme, ownerState }) => ({ + maxHeight: DIGITAL_CLOCK_VIEW_HEIGHT, + width: 56, + padding: 0, + overflow: 'hidden', + scrollBehavior: ownerState.alreadyRendered ? 'smooth' : 'auto', + '&:hover': { + overflowY: 'auto', + }, + '&:not(:first-of-type)': { + borderLeft: `1px solid ${(theme.vars || theme).palette.divider}`, + }, + '&:after': { + display: 'block', + content: '""', + height: 188, + }, + }), +); + +const MultiSectionDigitalClockSectionItem = styled(MenuItem, { + name: 'MuiMultiSectionDigitalClockSection', + slot: 'Item', + overridesResolver: (_, styles) => styles.item, +})(({ theme }) => ({ + padding: 8, + margin: '2px 4px', + width: 48, + justifyContent: 'center', + '&:first-of-type': { + marginTop: 4, + }, + '&:hover': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity), + }, + '&.Mui-selected': { + backgroundColor: (theme.vars || theme).palette.primary.main, + color: (theme.vars || theme).palette.primary.contrastText, + '&:focus-visible, &:hover': { + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, + '&.Mui-focusVisible': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.focusOpacity})` + : alpha(theme.palette.primary.main, theme.palette.action.focusOpacity), + }, +})); + +type MultiSectionDigitalClockSectionComponent = ( + props: MultiSectionDigitalClockSectionProps & React.RefAttributes, +) => JSX.Element & { propTypes?: any }; + +/** + * @ignore - internal component. + */ +export const MultiSectionDigitalClockSection = React.forwardRef( + function MultiSectionDigitalClockSection( + inProps: MultiSectionDigitalClockSectionProps, + ref: React.Ref, + ) { + const containerRef = React.useRef(null); + const handleRef = useForkRef(ref, containerRef); + + const props = useThemeProps({ + props: inProps, + name: 'MuiMultiSectionDigitalClockSection', + }); + + const { + autoFocus, + onChange, + className, + disabled, + readOnly, + items, + active, + slots, + slotProps, + skipDisabled, + ...other + } = props; + + const ownerState = React.useMemo( + () => ({ ...props, alreadyRendered: !!containerRef.current }), + [props], + ); + const classes = useUtilityClasses(ownerState); + const DigitalClockSectionItem = + slots?.digitalClockSectionItem ?? MultiSectionDigitalClockSectionItem; + + React.useEffect(() => { + if (containerRef.current === null) { + return; + } + const selectedItem = containerRef.current.querySelector( + '[role="option"][aria-selected="true"]', + ); + if (!selectedItem) { + return; + } + if (active && autoFocus) { + selectedItem.focus(); + } + const offsetTop = selectedItem.offsetTop; + + // Subtracting the 4px of extra margin intended for the first visible section item + containerRef.current.scrollTop = offsetTop - 4; + }); + + return ( + + {items.map((option) => { + if (skipDisabled && option.isDisabled?.(option.value)) { + return null; + } + const isSelected = option.isSelected(option.value); + return ( + !readOnly && onChange(option.value)} + selected={isSelected} + disabled={disabled ?? option.isDisabled?.(option.value)} + disableRipple={readOnly} + role="option" + // aria-readonly is not supported here and does not have any effect + aria-disabled={readOnly} + aria-label={option.ariaLabel} + aria-selected={isSelected} + {...slotProps?.digitalClockSectionItem} + > + {option.label} + + ); + })} + + ); + }, +) as MultiSectionDigitalClockSectionComponent; diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/index.ts b/packages/x-date-pickers/src/MultiSectionDigitalClock/index.ts new file mode 100644 index 000000000000..1e3d5b097499 --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/index.ts @@ -0,0 +1,21 @@ +export { MultiSectionDigitalClock } from './MultiSectionDigitalClock'; +export type { + MultiSectionDigitalClockProps, + MultiSectionDigitalClockSlotsComponent, + MultiSectionDigitalClockSlotsComponentsProps, +} from './MultiSectionDigitalClock.types'; + +export { multiSectionDigitalClockSectionClasses } from './multiSectionDigitalClockSectionClasses'; +export type { + MultiSectionDigitalClockSectionClasses, + MultiSectionDigitalClockSectionClassKey, +} from './multiSectionDigitalClockSectionClasses'; +export type { ExportedMultiSectionDigitalClockSectionProps } from './MultiSectionDigitalClockSection'; +export { + multiSectionDigitalClockClasses, + getMultiSectionDigitalClockUtilityClass, +} from './multiSectionDigitalClockClasses'; +export type { + MultiSectionDigitalClockClasses, + MultiSectionDigitalClockClassKey, +} from './multiSectionDigitalClockClasses'; diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/multiSectionDigitalClockClasses.ts b/packages/x-date-pickers/src/MultiSectionDigitalClock/multiSectionDigitalClockClasses.ts new file mode 100644 index 000000000000..84a4c2e3b564 --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/multiSectionDigitalClockClasses.ts @@ -0,0 +1,16 @@ +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; + +export interface MultiSectionDigitalClockClasses { + /** Styles applied to the root element. */ + root: string; +} + +export type MultiSectionDigitalClockClassKey = keyof MultiSectionDigitalClockClasses; + +export function getMultiSectionDigitalClockUtilityClass(slot: string) { + return generateUtilityClass('MuiMultiSectionDigitalClock', slot); +} + +export const multiSectionDigitalClockClasses: MultiSectionDigitalClockClasses = + generateUtilityClasses('MuiMultiSectionDigitalClock', ['root']); diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/multiSectionDigitalClockSectionClasses.ts b/packages/x-date-pickers/src/MultiSectionDigitalClock/multiSectionDigitalClockSectionClasses.ts new file mode 100644 index 000000000000..97721ba6ef3a --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/multiSectionDigitalClockSectionClasses.ts @@ -0,0 +1,18 @@ +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; + +export interface MultiSectionDigitalClockSectionClasses { + /** Styles applied to the root (list) element. */ + root: string; + /** Styles applied to the list item (by default: MenuItem) element. */ + item: string; +} + +export type MultiSectionDigitalClockSectionClassKey = keyof MultiSectionDigitalClockSectionClasses; + +export function getMultiSectionDigitalClockSectionUtilityClass(slot: string) { + return generateUtilityClass('MuiMultiSectionDigitalClock', slot); +} + +export const multiSectionDigitalClockSectionClasses: MultiSectionDigitalClockSectionClasses = + generateUtilityClasses('MuiMultiSectionDigitalClock', ['root', 'item']); diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx new file mode 100644 index 000000000000..02cb357fb8cb --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { screen, userEvent, describeConformance } from '@mui/monorepo/test/utils'; +import { describeValidation } from '@mui/x-date-pickers/tests/describeValidation'; +import { describeValue } from '@mui/x-date-pickers/tests/describeValue'; +import { createPickerRenderer, adapterToUse, wrapPickerMount } from 'test/utils/pickers-utils'; +import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; +import { expect } from 'chai'; + +describe(' - Describes', () => { + const { render, clock } = createPickerRenderer({ clock: 'fake' }); + + describeValidation(MultiSectionDigitalClock, () => ({ + render, + clock, + views: ['hours', 'minutes'], + componentFamily: 'multi-section-digital-clock', + variant: 'desktop', + })); + + describeConformance(, () => ({ + classes: {} as any, + render, + muiName: 'MuiMultiSectionDigitalClock', + wrapMount: wrapPickerMount, + refInstanceof: window.HTMLDivElement, + skip: [ + 'componentProp', + 'componentsProp', + 'themeDefaultProps', + 'themeStyleOverrides', + 'themeVariants', + 'mergeClassName', + 'propsSpread', + 'rootClass', + 'reactTestRenderer', + ], + })); + + describeValue(MultiSectionDigitalClock, () => ({ + render, + componentFamily: 'multi-section-digital-clock', + type: 'time', + variant: 'desktop', + values: [ + adapterToUse.date(new Date(2018, 0, 1, 15, 30)), + adapterToUse.date(new Date(2018, 0, 1, 16, 15)), + ], + emptyValue: null, + clock, + assertRenderedValue: (expectedValue: any) => { + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const selectedItems = screen.queryAllByRole('option', { selected: true }); + if (!expectedValue) { + expect(selectedItems).to.have.length(0); + } else { + const hoursLabel = adapterToUse.format( + expectedValue, + hasMeridiem ? 'hours12h' : 'hours24h', + ); + const minutesLabel = adapterToUse.getMinutes(expectedValue).toString(); + expect(selectedItems[0]).to.have.text(hoursLabel); + expect(selectedItems[1]).to.have.text(minutesLabel); + if (hasMeridiem) { + expect(selectedItems[2]).to.have.text( + adapterToUse.getMeridiemText(adapterToUse.getHours(expectedValue) > 12 ? 'pm' : 'am'), + ); + } + } + }, + setNewValue: (value) => { + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const hoursLabel = parseInt( + adapterToUse.format(value, hasMeridiem ? 'hours12h' : 'hours24h'), + 10, + ); + const minutesLabel = adapterToUse.getMinutes(value).toString(); + userEvent.mousePress(screen.getByRole('option', { name: `${hoursLabel} hours` })); + userEvent.mousePress(screen.getByRole('option', { name: `${minutesLabel} minutes` })); + if (hasMeridiem) { + userEvent.mousePress( + screen.getByRole('option', { + name: adapterToUse.getMeridiemText(adapterToUse.getHours(value) > 12 ? 'pm' : 'am'), + }), + ); + } + + return value; + }, + })); +}); diff --git a/packages/x-date-pickers/src/PickersLayout/PickersLayout.tsx b/packages/x-date-pickers/src/PickersLayout/PickersLayout.tsx index 4bbf908fbccb..5a619a87f4a8 100644 --- a/packages/x-date-pickers/src/PickersLayout/PickersLayout.tsx +++ b/packages/x-date-pickers/src/PickersLayout/PickersLayout.tsx @@ -6,7 +6,7 @@ import { unstable_composeClasses as composeClasses } from '@mui/utils'; import { PickersLayoutProps } from './PickersLayout.types'; import { pickersLayoutClasses, getPickersLayoutUtilityClass } from './pickersLayoutClasses'; import usePickerLayout from './usePickerLayout'; -import { DateOrTimeView } from '../models'; +import { DateOrTimeViewWithMeridiem } from '../internals/models'; const useUtilityClasses = (ownerState: PickersLayoutProps) => { const { isLandscape, classes } = ownerState; @@ -70,9 +70,11 @@ export const PickersLayoutContentWrapper = styled('div', { flexDirection: 'column', }); -const PickersLayout = function PickersLayout( - inProps: PickersLayoutProps, -) { +const PickersLayout = function PickersLayout< + TValue, + TDate, + TView extends DateOrTimeViewWithMeridiem, +>(inProps: PickersLayoutProps) { const props = useThemeProps({ props: inProps, name: 'MuiPickersLayout' }); const { toolbar, content, tabs, actionBar, shortcuts } = usePickerLayout(props); @@ -161,9 +163,9 @@ PickersLayout.propTypes = { PropTypes.object, ]), value: PropTypes.any, - view: PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']), + view: PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']), views: PropTypes.arrayOf( - PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']).isRequired, + PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']).isRequired, ).isRequired, wrapperVariant: PropTypes.oneOf(['desktop', 'mobile']), } as any; diff --git a/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts b/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts index fca18cf465fa..1b9a223a5c54 100644 --- a/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts +++ b/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts @@ -2,16 +2,19 @@ import * as React from 'react'; import { SxProps, Theme } from '@mui/material/styles'; import { SlotComponentProps } from '@mui/base/utils'; import { PickersActionBarProps } from '../PickersActionBar'; -import { DateOrTimeView } from '../models'; import { BaseToolbarProps, ExportedBaseToolbarProps } from '../internals/models/props/toolbar'; import { BaseTabsProps, ExportedBaseTabsProps } from '../internals/models/props/tabs'; import { UsePickerLayoutPropsResponseLayoutProps } from '../internals/hooks/usePicker/usePickerLayoutProps'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; import { PickersLayoutClasses } from './pickersLayoutClasses'; -import { WrapperVariant } from '../internals/models/common'; +import { DateOrTimeViewWithMeridiem, WrapperVariant } from '../internals/models/common'; import { PickersShortcutsProps } from '../PickersShortcuts'; -export interface ExportedPickersLayoutSlotsComponent { +export interface ExportedPickersLayoutSlotsComponent< + TValue, + TDate, + TView extends DateOrTimeViewWithMeridiem, +> { /** * Custom component for the action bar, it is placed below the picker views. * @default PickersActionBar @@ -31,7 +34,7 @@ export interface ExportedPickersLayoutSlotsComponent; } -interface PickersLayoutActionBarOwnerState +interface PickersLayoutActionBarOwnerState extends PickersLayoutProps { wrapperVariant: WrapperVariant; } @@ -43,7 +46,7 @@ interface PickersShortcutsOwnerState extends PickersShortcutsProps { /** * Props passed down to the action bar component. @@ -69,8 +72,11 @@ export interface ExportedPickersLayoutSlotsComponentsProps< layout?: Partial>; } -export interface PickersLayoutSlotsComponent - extends ExportedPickersLayoutSlotsComponent { +export interface PickersLayoutSlotsComponent< + TValue, + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends ExportedPickersLayoutSlotsComponent { /** * Tabs enabling toggling between views. */ @@ -82,8 +88,11 @@ export interface PickersLayoutSlotsComponent>; } -export interface PickersLayoutSlotsComponentsProps - extends ExportedPickersLayoutSlotsComponentsProps { +export interface PickersLayoutSlotsComponentsProps< + TValue, + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends ExportedPickersLayoutSlotsComponentsProps { /** * Props passed down to the tabs component. */ @@ -94,7 +103,7 @@ export interface PickersLayoutSlotsComponentsProps +export interface PickersLayoutProps extends Omit, 'value'> { value?: TValue; className?: string; diff --git a/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx b/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx index 608c11237172..57969b59b716 100644 --- a/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx +++ b/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx @@ -4,12 +4,12 @@ import { unstable_composeClasses as composeClasses } from '@mui/utils'; import { PickersActionBar, PickersActionBarAction } from '../PickersActionBar'; import { PickersLayoutProps, SubComponents } from './PickersLayout.types'; import { getPickersLayoutUtilityClass } from './pickersLayoutClasses'; -import { DateOrTimeView } from '../models'; import { PickersShortcuts } from '../PickersShortcuts'; import { BaseToolbarProps } from '../internals/models/props/toolbar'; import { uncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { DateOrTimeViewWithMeridiem } from '../internals/models'; -function toolbarHasView( +function toolbarHasView( toolbarProps: BaseToolbarProps | any, ): toolbarProps is BaseToolbarProps { return toolbarProps.view !== null; @@ -30,13 +30,16 @@ const useUtilityClasses = (ownerState: PickersLayoutProps) => { return composeClasses(slots, getPickersLayoutUtilityClass, classes); }; -interface PickersLayoutPropsWithValueRequired - extends PickersLayoutProps { +interface PickersLayoutPropsWithValueRequired< + TValue, + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends PickersLayoutProps { value: TValue; } interface UsePickerLayoutResponse extends SubComponents {} -const usePickerLayout = ( +const usePickerLayout = ( props: PickersLayoutProps, ): UsePickerLayoutResponse => { const { diff --git a/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx b/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx index 2a0006f2bcf5..dd6e8b27c4b8 100644 --- a/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx +++ b/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx @@ -17,10 +17,11 @@ const StaticTimePicker = React.forwardRef(function StaticTimePicker( inProps: StaticTimePickerProps, ref: React.Ref, ) { - const defaultizedProps = useTimePickerDefaultizedProps>( - inProps, - 'MuiStaticTimePicker', - ); + const defaultizedProps = useTimePickerDefaultizedProps< + TDate, + TimeView, + StaticTimePickerProps + >(inProps, 'MuiStaticTimePicker'); const displayStaticWrapperAs = defaultizedProps.displayStaticWrapperAs ?? 'mobile'; const ampmInClock = defaultizedProps.ampmInClock ?? displayStaticWrapperAs === 'desktop'; diff --git a/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.types.ts b/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.types.ts index 13b91db76392..28c08d303b01 100644 --- a/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.types.ts +++ b/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.types.ts @@ -20,7 +20,7 @@ export interface StaticTimePickerSlotsComponentsProps UseStaticPickerSlotsComponentsProps {} export interface StaticTimePickerProps - extends BaseTimePickerProps, + extends BaseTimePickerProps, MakeOptional { /** * Overridable components. diff --git a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx index 7b95d7de5d13..e074df8b0a51 100644 --- a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx +++ b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx @@ -99,6 +99,8 @@ export const TimeClock = React.forwardRef(function TimeClock - autoFocus={autoFocus} + autoFocus={autoFocus ?? !!focusedView} ampmInClock={ampmInClock && views.includes('hours')} value={value} type={view} @@ -396,7 +400,10 @@ TimeClock.propTypes = { */ ampmInClock: PropTypes.bool, /** - * Set to `true` if focus should be moved to clock picker. + * If `true`, the main element is focused during the first mount. + * This main element is: + * - the element chosen by the visible view if any (i.e: the selected day on the `day` view). + * - the `input` element if there is a field rendered. */ autoFocus: PropTypes.bool, /** @@ -422,7 +429,7 @@ TimeClock.propTypes = { */ defaultValue: PropTypes.any, /** - * If `true`, the picker and text field are disabled. + * If `true`, the picker views and text field are disabled. * @default false */ disabled: PropTypes.bool, @@ -441,6 +448,10 @@ TimeClock.propTypes = { * @default false */ disablePast: PropTypes.bool, + /** + * Controlled focused view. + */ + focusedView: PropTypes.oneOf(['hours', 'minutes', 'seconds']), /** * Maximal selectable time. * The date part of the object will be ignored unless `props.disableIgnoringDatePartForTimeValidation === true`. @@ -461,20 +472,30 @@ TimeClock.propTypes = { * @template TDate * @param {TDate | null} value The new value. * @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete. + * @param {TView | undefined} selectedView Indicates the view in which the selection has been made. */ onChange: PropTypes.func, + /** + * Callback fired on focused view change. + * @template TView + * @param {TView} view The new view to focus or not. + * @param {boolean} hasFocus `true` if the view should be focused. + */ + onFocusedViewChange: PropTypes.func, /** * Callback fired on view change. - * @param {TimeView} view The new view. + * @template TView + * @param {TView} view The new view. */ onViewChange: PropTypes.func, /** - * Initially open view. - * @default 'hours' + * The default visible view. + * Used when the component view is not controlled. + * Must be a valid option from `views` list. */ openTo: PropTypes.oneOf(['hours', 'minutes', 'seconds']), /** - * Make picker read only. + * If `true`, the picker views and text field are read-only. * @default false */ readOnly: PropTypes.bool, @@ -519,12 +540,13 @@ TimeClock.propTypes = { */ value: PropTypes.any, /** - * Controlled open view. + * The visible view. + * Used when the component view is controlled. + * Must be a valid option from `views` list. */ view: PropTypes.oneOf(['hours', 'minutes', 'seconds']), /** - * Views for calendar picker. - * @default ['hours', 'minutes'] + * Available views. */ views: PropTypes.arrayOf(PropTypes.oneOf(['hours', 'minutes', 'seconds']).isRequired), } as any; diff --git a/packages/x-date-pickers/src/TimeClock/TimeClock.types.ts b/packages/x-date-pickers/src/TimeClock/TimeClock.types.ts index 7769ce0626b3..8f02aa2d09a1 100644 --- a/packages/x-date-pickers/src/TimeClock/TimeClock.types.ts +++ b/packages/x-date-pickers/src/TimeClock/TimeClock.types.ts @@ -1,23 +1,13 @@ -import { SxProps } from '@mui/system'; -import { Theme } from '@mui/material/styles'; import { TimeClockClasses } from './timeClockClasses'; -import { TimeValidationProps, BaseTimeValidationProps } from '../internals/models/validation'; import { PickersArrowSwitcherSlotsComponent, PickersArrowSwitcherSlotsComponentsProps, } from '../internals/components/PickersArrowSwitcher'; -import { TimeView } from '../models'; -import { PickerSelectionState } from '../internals/hooks/usePicker/usePickerValue.types'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { BaseClockProps, ExportedBaseClockProps } from '../internals/models/props/clock'; +import { TimeView } from '../models'; -export interface ExportedTimeClockProps - extends TimeValidationProps, - BaseTimeValidationProps { - /** - * 12h/24h view for hour selection clock. - * @default `utils.is12HourCycleInCurrentLocale()` - */ - ampm?: boolean; +export interface ExportedTimeClockProps extends ExportedBaseClockProps { /** * Display ampm controls under the clock (instead of in the toolbar). * @default false @@ -29,16 +19,9 @@ export interface TimeClockSlotsComponent extends PickersArrowSwitcherSlotsCompon export interface TimeClockSlotsComponentsProps extends PickersArrowSwitcherSlotsComponentsProps {} -export interface TimeClockProps extends ExportedTimeClockProps { - className?: string; - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; - /** - * Set to `true` if focus should be moved to clock picker. - */ - autoFocus?: boolean; +export interface TimeClockProps + extends ExportedTimeClockProps, + BaseClockProps { /** * Override or extend the styles applied to the component. */ @@ -65,51 +48,5 @@ export interface TimeClockProps extends ExportedTimeClockProps { * @default {} */ slotProps?: TimeClockSlotsComponentsProps; - /** - * The selected value. - * Used when the component is controlled. - */ - value?: TDate | null; - /** - * The default selected value. - * Used when the component is not controlled. - */ - defaultValue?: TDate | null; - /** - * Callback fired when the value changes. - * @template TDate - * @param {TDate | null} value The new value. - * @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete. - */ - onChange?: (value: TDate | null, selectionState?: PickerSelectionState) => void; showViewSwitcher?: boolean; - /** - * Controlled open view. - */ - view?: TimeView; - /** - * Views for calendar picker. - * @default ['hours', 'minutes'] - */ - views?: readonly TimeView[]; - /** - * Callback fired on view change. - * @param {TimeView} view The new view. - */ - onViewChange?: (view: TimeView) => void; - /** - * Initially open view. - * @default 'hours' - */ - openTo?: TimeView; - /** - * If `true`, the picker and text field are disabled. - * @default false - */ - disabled?: boolean; - /** - * Make picker read only. - * @default false - */ - readOnly?: boolean; } diff --git a/packages/x-date-pickers/src/TimePicker/TimePicker.tsx b/packages/x-date-pickers/src/TimePicker/TimePicker.tsx index 492e0a99a013..e3fc8cdb3251 100644 --- a/packages/x-date-pickers/src/TimePicker/TimePicker.tsx +++ b/packages/x-date-pickers/src/TimePicker/TimePicker.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useThemeProps } from '@mui/material/styles'; import { DesktopTimePicker } from '../DesktopTimePicker'; -import { MobileTimePicker } from '../MobileTimePicker'; +import { MobileTimePicker, MobileTimePickerProps } from '../MobileTimePicker'; import { TimePickerProps } from './TimePicker.types'; import { DEFAULT_DESKTOP_MODE_MEDIA_QUERY } from '../internals/utils/utils'; @@ -26,7 +26,7 @@ const TimePicker = React.forwardRef(function TimePicker( return ; } - return ; + return )} />; }) as TimePickerComponent; TimePicker.propTypes = { @@ -207,7 +207,7 @@ TimePicker.propTypes = { * Used when the component view is not controlled. * Must be a valid option from `views` list. */ - openTo: PropTypes.oneOf(['hours', 'minutes', 'seconds']), + openTo: PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']), /** * Force rendering in particular orientation. */ @@ -256,6 +256,11 @@ TimePicker.propTypes = { * @returns {boolean} If `true` the time will be disabled. */ shouldDisableTime: PropTypes.func, + /** + * If `true`, disabled digital clock items will not be rendered. + * @default false + */ + skipDisabled: PropTypes.bool, /** * The props used for each component slot. * @default {} @@ -274,6 +279,22 @@ TimePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Amount of time options below or at which the single column time renderer is used. + * @default 24 + */ + thresholdToRenderTimeInASingleColumn: PropTypes.number, + /** + * The time steps between two time unit options. + * For example, if `timeStep.minutes = 8`, then the available minute options will be `[0, 8, 16, 24, 32, 40, 48, 56]`. + * When single column time renderer is used, only `timeStep.minutes` will be used. + * @default{ hours: 1, minutes: 5, seconds: 5 } + */ + timeSteps: PropTypes.shape({ + hours: PropTypes.number, + minutes: PropTypes.number, + seconds: PropTypes.number, + }), /** * The selected value. * Used when the component is controlled. @@ -284,7 +305,7 @@ TimePicker.propTypes = { * Used when the component view is controlled. * Must be a valid option from `views` list. */ - view: PropTypes.oneOf(['hours', 'minutes', 'seconds']), + view: PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']), /** * Define custom view renderers for each section. * If `null`, the section will only have field editing. @@ -292,6 +313,7 @@ TimePicker.propTypes = { */ viewRenderers: PropTypes.shape({ hours: PropTypes.func, + meridiem: PropTypes.func, minutes: PropTypes.func, seconds: PropTypes.func, }), diff --git a/packages/x-date-pickers/src/TimePicker/TimePicker.types.ts b/packages/x-date-pickers/src/TimePicker/TimePicker.types.ts index 02a316ed2044..6116ab13f0b1 100644 --- a/packages/x-date-pickers/src/TimePicker/TimePicker.types.ts +++ b/packages/x-date-pickers/src/TimePicker/TimePicker.types.ts @@ -3,6 +3,7 @@ import { DesktopTimePickerSlotsComponent, DesktopTimePickerSlotsComponentsProps, } from '../DesktopTimePicker'; +import { TimeViewWithMeridiem } from '../internals/models'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; import { MobileTimePickerProps, @@ -12,15 +13,15 @@ import { export interface TimePickerSlotsComponents extends DesktopTimePickerSlotsComponent, - MobileTimePickerSlotsComponent {} + MobileTimePickerSlotsComponent {} export interface TimePickerSlotsComponentsProps extends DesktopTimePickerSlotsComponentsProps, - MobileTimePickerSlotsComponentsProps {} + MobileTimePickerSlotsComponentsProps {} export interface TimePickerProps extends DesktopTimePickerProps, - MobileTimePickerProps { + Omit, 'views'> { /** * CSS media query when `Mobile` mode will be changed to `Desktop`. * @default '@media (pointer: fine)' diff --git a/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx b/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx index 0072af2aec8c..0c1bc44bbfa1 100644 --- a/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx +++ b/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx @@ -15,9 +15,10 @@ import { timePickerToolbarClasses, TimePickerToolbarClasses, } from './timePickerToolbarClasses'; -import { TimeView } from '../models'; +import { TimeViewWithMeridiem } from '../internals/models'; -export interface TimePickerToolbarProps extends BaseToolbarProps { +export interface TimePickerToolbarProps + extends BaseToolbarProps { ampm?: boolean; ampmInClock?: boolean; classes?: Partial; @@ -297,10 +298,9 @@ TimePickerToolbar.propTypes = { /** * Currently visible picker view. */ - view: PropTypes.oneOf(['hours', 'minutes', 'seconds']).isRequired, - views: PropTypes.arrayOf( - PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']).isRequired, - ).isRequired, + view: PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']).isRequired, + views: PropTypes.arrayOf(PropTypes.oneOf(['hours', 'meridiem', 'minutes', 'seconds']).isRequired) + .isRequired, } as any; export { TimePickerToolbar }; diff --git a/packages/x-date-pickers/src/TimePicker/shared.tsx b/packages/x-date-pickers/src/TimePicker/shared.tsx index df677b8dad32..e71fd3795785 100644 --- a/packages/x-date-pickers/src/TimePicker/shared.tsx +++ b/packages/x-date-pickers/src/TimePicker/shared.tsx @@ -5,7 +5,6 @@ import { useUtils } from '../internals/hooks/useUtils'; import { TimeClockSlotsComponent, TimeClockSlotsComponentsProps, - ExportedTimeClockProps, } from '../TimeClock/TimeClock.types'; import { BasePickerInputProps } from '../internals/models/props/basePickerProps'; import { BaseTimeValidationProps } from '../internals/models/validation'; @@ -15,11 +14,13 @@ import { ExportedTimePickerToolbarProps, TimePickerToolbar, } from './TimePickerToolbar'; -import { TimeValidationError, TimeView } from '../models'; +import { TimeValidationError } from '../models'; import { PickerViewRendererLookup } from '../internals/hooks/usePicker/usePickerViews'; import { TimeViewRendererProps } from '../timeViewRenderers'; import { applyDefaultViewProps } from '../internals/utils/views'; import { uncapitalizeObjectKeys, UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { BaseClockProps, ExportedBaseClockProps } from '../internals/models/props/clock'; +import { TimeViewWithMeridiem } from '../internals/models'; export interface BaseTimePickerSlotsComponent extends TimeClockSlotsComponent { /** @@ -33,9 +34,9 @@ export interface BaseTimePickerSlotsComponentsProps extends TimeClockSlotsCompon toolbar?: ExportedTimePickerToolbarProps; } -export interface BaseTimePickerProps - extends BasePickerInputProps, - ExportedTimeClockProps { +export interface BaseTimePickerProps + extends BasePickerInputProps, + ExportedBaseClockProps { /** * Display ampm controls under the clock (instead of in the toolbar). * @default true on desktop, false on mobile @@ -69,13 +70,19 @@ export interface BaseTimePickerProps * If `undefined`, internally defined view will be the used. */ viewRenderers?: Partial< - PickerViewRendererLookup, {}> + PickerViewRendererLookup< + TDate | null, + TView, + TimeViewRendererProps>, + {} + > >; } type UseTimePickerDefaultizedProps< TDate, - Props extends BaseTimePickerProps, + TView extends TimeViewWithMeridiem, + Props extends BaseTimePickerProps, > = LocalizedComponent< TDate, Omit< @@ -84,10 +91,11 @@ type UseTimePickerDefaultizedProps< > >; -export function useTimePickerDefaultizedProps>( - props: Props, - name: string, -): UseTimePickerDefaultizedProps { +export function useTimePickerDefaultizedProps< + TDate, + TView extends TimeViewWithMeridiem, + Props extends BaseTimePickerProps, +>(props: Props, name: string): UseTimePickerDefaultizedProps { const utils = useUtils(); const themeProps = useThemeProps({ props, @@ -116,8 +124,8 @@ export function useTimePickerDefaultizedProps view === 'year' || view === 'month' || view === 'day'; -export interface DateViewRendererProps +export interface DateViewRendererProps extends Omit< DateCalendarProps, 'views' | 'openTo' | 'view' | 'onViewChange' | 'focusedView' diff --git a/packages/x-date-pickers/src/index.ts b/packages/x-date-pickers/src/index.ts index 0ed92c2d5140..bc8c4ff7974a 100644 --- a/packages/x-date-pickers/src/index.ts +++ b/packages/x-date-pickers/src/index.ts @@ -1,4 +1,8 @@ +// Clocks export * from './TimeClock'; +export * from './DigitalClock'; +export * from './MultiSectionDigitalClock'; + export * from './LocalizationProvider'; export * from './PickersDay'; export * from './locales'; diff --git a/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx b/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx index cd4b26513c6a..6047fd0839aa 100644 --- a/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx +++ b/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx @@ -6,9 +6,9 @@ import { styled, useThemeProps } from '@mui/material/styles'; import { unstable_composeClasses as composeClasses } from '@mui/utils'; import { BaseToolbarProps } from '../models/props/toolbar'; import { getPickersToolbarUtilityClass, PickersToolbarClasses } from './pickersToolbarClasses'; -import { DateOrTimeView } from '../../models'; +import { DateOrTimeViewWithMeridiem } from '../models'; -export interface PickersToolbarProps +export interface PickersToolbarProps extends Pick, 'isLandscape' | 'hidden' | 'titleId'> { className?: string; landscapeDirection?: 'row' | 'column'; @@ -61,14 +61,14 @@ const PickersToolbarContent = styled(Grid, { }), })); -type PickersToolbarComponent = (( +type PickersToolbarComponent = (( props: React.PropsWithChildren> & React.RefAttributes, ) => JSX.Element) & { propTypes?: any }; export const PickersToolbar = React.forwardRef(function PickersToolbar< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, >( inProps: React.PropsWithChildren>, ref: React.Ref, diff --git a/packages/x-date-pickers/src/internals/constants/dimensions.ts b/packages/x-date-pickers/src/internals/constants/dimensions.ts index 62c797505549..1542c38c35eb 100644 --- a/packages/x-date-pickers/src/internals/constants/dimensions.ts +++ b/packages/x-date-pickers/src/internals/constants/dimensions.ts @@ -2,3 +2,4 @@ export const DAY_SIZE = 36; export const DAY_MARGIN = 2; export const DIALOG_WIDTH = 320; export const VIEW_HEIGHT = 358; +export const DIGITAL_CLOCK_VIEW_HEIGHT = 232; diff --git a/packages/x-date-pickers/src/internals/demo/DemoContainer.tsx b/packages/x-date-pickers/src/internals/demo/DemoContainer.tsx index 73ddce0722bc..798ef3520c63 100644 --- a/packages/x-date-pickers/src/internals/demo/DemoContainer.tsx +++ b/packages/x-date-pickers/src/internals/demo/DemoContainer.tsx @@ -15,6 +15,7 @@ type PickersGridChildComponentType = | 'multi-input-range-field' | 'single-input-range-field' | 'UI-view' + | 'Tall-UI-view' | 'multi-panel-UI-view'; type PickersSupportedSections = 'date' | 'time' | 'date-time'; @@ -24,6 +25,10 @@ const getChildTypeFromChildName = (childName: string): PickersGridChildComponent return 'multi-panel-UI-view'; } + if (childName.match(/^([A-Za-z]*)(DigitalClock)$/)) { + return 'Tall-UI-view'; + } + if (childName.match(/^Static([A-Za-z]+)/) || childName.match(/^([A-Za-z]+)(Calendar|Clock)$/)) { return 'UI-view'; } @@ -98,7 +103,7 @@ export function DemoContainer(props: DemoGridProps) { const getSpacing = (direction: 'column' | 'row') => { if (direction === 'row') { - return childrenTypes.has('UI-view') ? 3 : 2; + return childrenTypes.has('UI-view') || childrenTypes.has('Tall-UI-view') ? 3 : 2; } return childrenTypes.has('UI-view') ? 4 : 3; diff --git a/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx b/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx index b73739a40d7c..a53023ff122a 100644 --- a/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx +++ b/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { useUtils } from './useUtils'; import { PickerOnChangeFn } from './useViews'; import { getMeridiem, convertToMeridiem } from '../utils/time-utils'; +import { PickerSelectionState } from './usePicker'; interface MonthValidationOptions { disablePast?: boolean; @@ -43,6 +44,7 @@ export function useMeridiemMode( date: TDate | null, ampm: boolean | undefined, onChange: PickerOnChangeFn, + selectionState?: PickerSelectionState, ) { const utils = useUtils(); const meridiemMode = getMeridiem(date, utils); @@ -51,9 +53,9 @@ export function useMeridiemMode( (mode: 'am' | 'pm') => { const timeWithMeridiem = date == null ? null : convertToMeridiem(date, mode, Boolean(ampm), utils); - onChange(timeWithMeridiem, 'partial'); + onChange(timeWithMeridiem, selectionState ?? 'partial'); }, - [ampm, date, onChange, utils], + [ampm, date, onChange, selectionState, utils], ); return { meridiemMode, handleMeridiemChange }; diff --git a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx index 217da4e09a1c..970cc59a9080 100644 --- a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx @@ -11,7 +11,8 @@ import { usePicker } from '../usePicker'; import { LocalizationProvider } from '../../../LocalizationProvider'; import { PickersLayout } from '../../../PickersLayout'; import { InferError } from '../useValidation'; -import { FieldSection, BaseSingleInputFieldProps, DateOrTimeView } from '../../../models'; +import { FieldSection, BaseSingleInputFieldProps } from '../../../models'; +import { DateOrTimeViewWithMeridiem } from '../../models'; /** * Hook managing all the single-date desktop pickers: @@ -21,7 +22,7 @@ import { FieldSection, BaseSingleInputFieldProps, DateOrTimeView } from '../../. */ export const useDesktopPicker = < TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseDesktopPickerProps, >({ props, @@ -110,6 +111,7 @@ export const useDesktopPicker = < formatDensity, label, autoFocus: autoFocus && !props.open, + focused: open ? true : undefined, }, ownerState: props, }); diff --git a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.types.ts b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.types.ts index 27819832b625..f19c210f4c19 100644 --- a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.types.ts @@ -13,12 +13,7 @@ import { PickersPopperSlotsComponentsProps, } from '../../components/PickersPopper'; import { UsePickerParams, UsePickerProps } from '../usePicker'; -import { - BaseSingleInputFieldProps, - FieldSection, - DateOrTimeView, - MuiPickersAdapter, -} from '../../../models'; +import { BaseSingleInputFieldProps, FieldSection, MuiPickersAdapter } from '../../../models'; import { ExportedPickersLayoutSlotsComponent, ExportedPickersLayoutSlotsComponentsProps, @@ -27,8 +22,9 @@ import { import { UsePickerValueNonStaticProps } from '../usePicker/usePickerValue.types'; import { UsePickerViewsNonStaticProps, UsePickerViewsProps } from '../usePicker/usePickerViews'; import { UncapitalizeObjectKeys } from '../../utils/slots-migration'; +import { DateOrTimeViewWithMeridiem } from '../../models'; -export interface UseDesktopPickerSlotsComponent +export interface UseDesktopPickerSlotsComponent extends Pick< PickersPopperSlotsComponent, 'DesktopPaper' | 'DesktopTransition' | 'DesktopTrapFocus' | 'Popper' @@ -60,12 +56,16 @@ export interface UseDesktopPickerSlotsComponent - extends ExportedUseDesktopPickerSlotsComponentsProps, +export interface UseDesktopPickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends ExportedUseDesktopPickerSlotsComponentsProps, Pick, 'toolbar'> {} -export interface ExportedUseDesktopPickerSlotsComponentsProps - extends PickersPopperSlotsComponentsProps, +export interface ExportedUseDesktopPickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends PickersPopperSlotsComponentsProps, ExportedPickersLayoutSlotsComponentsProps { field?: SlotComponentProps< React.ElementType>, @@ -95,7 +95,7 @@ export interface DesktopOnlyPickerProps export interface UseDesktopPickerProps< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, TExternalProps extends UsePickerViewsProps, > extends BasePickerProps, @@ -114,7 +114,7 @@ export interface UseDesktopPickerProps< export interface UseDesktopPickerParams< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseDesktopPickerProps, > extends Pick< UsePickerParams, diff --git a/packages/x-date-pickers/src/internals/hooks/useIsLandscape.tsx b/packages/x-date-pickers/src/internals/hooks/useIsLandscape.tsx index 7958aa95155e..200bb74b7cd2 100644 --- a/packages/x-date-pickers/src/internals/hooks/useIsLandscape.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useIsLandscape.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/utils'; import { arrayIncludes } from '../utils/utils'; -import { DateOrTimeView } from '../../models'; +import { DateOrTimeViewWithMeridiem } from '../models'; type Orientation = 'portrait' | 'landscape'; @@ -23,7 +23,7 @@ function getOrientation(): Orientation { } export const useIsLandscape = ( - views: readonly DateOrTimeView[], + views: readonly DateOrTimeViewWithMeridiem[], customOrientation: Orientation | undefined, ): boolean => { const [orientation, setOrientation] = React.useState(getOrientation); diff --git a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx index 373eca171b04..5f0187380290 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx @@ -10,7 +10,8 @@ import { useUtils } from '../useUtils'; import { LocalizationProvider } from '../../../LocalizationProvider'; import { PickersLayout } from '../../../PickersLayout'; import { InferError } from '../useValidation'; -import { FieldSection, BaseSingleInputFieldProps, DateOrTimeView } from '../../../models'; +import { FieldSection, BaseSingleInputFieldProps } from '../../../models'; +import { DateOrTimeViewWithMeridiem } from '../../models'; /** * Hook managing all the single-date mobile pickers: @@ -20,7 +21,7 @@ import { FieldSection, BaseSingleInputFieldProps, DateOrTimeView } from '../../. */ export const useMobilePicker = < TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseMobilePickerProps, >({ props, diff --git a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.types.ts b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.types.ts index 9c3756974b9b..769717b6523c 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.types.ts @@ -11,12 +11,7 @@ import { PickersModalDialogSlotsComponentsProps, } from '../../components/PickersModalDialog'; import { UsePickerParams, UsePickerProps } from '../usePicker'; -import { - BaseSingleInputFieldProps, - FieldSection, - DateOrTimeView, - MuiPickersAdapter, -} from '../../../models'; +import { BaseSingleInputFieldProps, FieldSection, MuiPickersAdapter } from '../../../models'; import { ExportedPickersLayoutSlotsComponent, ExportedPickersLayoutSlotsComponentsProps, @@ -25,8 +20,9 @@ import { import { UsePickerValueNonStaticProps } from '../usePicker/usePickerValue.types'; import { UsePickerViewsNonStaticProps, UsePickerViewsProps } from '../usePicker/usePickerViews'; import { UncapitalizeObjectKeys } from '../../utils/slots-migration'; +import { DateOrTimeViewWithMeridiem } from '../../models'; -export interface UseMobilePickerSlotsComponent +export interface UseMobilePickerSlotsComponent extends PickersModalDialogSlotsComponent, ExportedPickersLayoutSlotsComponent { /** @@ -41,8 +37,10 @@ export interface UseMobilePickerSlotsComponent; } -export interface ExportedUseMobilePickerSlotsComponentsProps - extends PickersModalDialogSlotsComponentsProps, +export interface ExportedUseMobilePickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends PickersModalDialogSlotsComponentsProps, ExportedPickersLayoutSlotsComponentsProps { field?: SlotComponentProps< React.ElementType>, @@ -52,8 +50,10 @@ export interface ExportedUseMobilePickerSlotsComponentsProps>; } -export interface UseMobilePickerSlotsComponentsProps - extends ExportedUseMobilePickerSlotsComponentsProps, +export interface UseMobilePickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends ExportedUseMobilePickerSlotsComponentsProps, Pick, 'toolbar'> {} export interface MobileOnlyPickerProps @@ -64,7 +64,7 @@ export interface MobileOnlyPickerProps export interface UseMobilePickerProps< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, TExternalProps extends UsePickerViewsProps, > extends BasePickerProps, @@ -83,7 +83,7 @@ export interface UseMobilePickerProps< export interface UseMobilePickerParams< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseMobilePickerProps, > extends Pick< UsePickerParams, diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts index eb09fd7a4798..3e53d8b8fc1c 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts @@ -4,7 +4,8 @@ import { usePickerViews } from './usePickerViews'; import { usePickerLayoutProps } from './usePickerLayoutProps'; import { InferError } from '../useValidation'; import { buildWarning } from '../../utils/warning'; -import { FieldSection, DateOrTimeView } from '../../../models'; +import { FieldSection } from '../../../models'; +import { DateOrTimeViewWithMeridiem } from '../../models'; const warnRenderInputIsDefined = buildWarning([ 'The `renderInput` prop has been removed in version 6.0 of the Date and Time Pickers.', @@ -15,7 +16,7 @@ const warnRenderInputIsDefined = buildWarning([ export const usePicker = < TValue, TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TSection extends FieldSection, TExternalProps extends UsePickerProps, TAdditionalProps extends {}, diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts index 059d2ef5ab21..0c00c3c7b8a2 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts @@ -11,14 +11,15 @@ import { UsePickerViewsBaseProps, } from './usePickerViews'; import { UsePickerLayoutProps, UsePickerLayoutPropsResponse } from './usePickerLayoutProps'; -import { FieldSection, DateOrTimeView } from '../../../models'; +import { FieldSection } from '../../../models'; +import { DateOrTimeViewWithMeridiem } from '../../models'; /** * Props common to all picker headless implementations. */ export interface UsePickerBaseProps< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, TExternalProps extends UsePickerViewsProps, TAdditionalProps extends {}, @@ -28,7 +29,7 @@ export interface UsePickerBaseProps< export interface UsePickerProps< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TSection extends FieldSection, TError, TExternalProps extends UsePickerViewsProps, @@ -40,7 +41,7 @@ export interface UsePickerProps< export interface UsePickerParams< TValue, TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TSection extends FieldSection, TExternalProps extends UsePickerProps, TAdditionalProps extends {}, @@ -57,7 +58,7 @@ export interface UsePickerParams< export interface UsePickerResponse< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TSection extends FieldSection, TError, > extends Omit, 'viewProps' | 'layoutProps'>, diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerLayoutProps.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerLayoutProps.ts index 0518469f86b3..db3a329d9e8f 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerLayoutProps.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerLayoutProps.ts @@ -1,8 +1,7 @@ import { useIsLandscape } from '../useIsLandscape'; import { UsePickerValueLayoutResponse } from './usePickerValue.types'; import { UsePickerViewsLayoutResponse } from './usePickerViews'; -import { DateOrTimeView } from '../../../models'; -import { WrapperVariant } from '../../models/common'; +import { DateOrTimeViewWithMeridiem, WrapperVariant } from '../../models/common'; /** * Props used to create the layout of the views. @@ -17,8 +16,10 @@ export interface UsePickerLayoutProps { orientation?: 'portrait' | 'landscape'; } -export interface UsePickerLayoutPropsResponseLayoutProps - extends UsePickerValueLayoutResponse, +export interface UsePickerLayoutPropsResponseLayoutProps< + TValue, + TView extends DateOrTimeViewWithMeridiem, +> extends UsePickerValueLayoutResponse, UsePickerViewsLayoutResponse, UsePickerLayoutProps { isLandscape: boolean; @@ -26,11 +27,11 @@ export interface UsePickerLayoutPropsResponseLayoutProps boolean; } -export interface UsePickerLayoutPropsResponse { +export interface UsePickerLayoutPropsResponse { layoutProps: UsePickerLayoutPropsResponseLayoutProps; } -export interface UsePickerLayoutPropsParams { +export interface UsePickerLayoutPropsParams { props: UsePickerLayoutProps; propsFromPickerValue: UsePickerValueLayoutResponse; propsFromPickerViews: UsePickerViewsLayoutResponse; @@ -40,7 +41,7 @@ export interface UsePickerLayoutPropsParams({ +export const usePickerLayoutProps = ({ props, propsFromPickerValue, propsFromPickerViews, diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerViews.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerViews.ts index 73d0fd220015..ac9a36cbd5b4 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerViews.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerViews.ts @@ -3,17 +3,17 @@ import { SxProps } from '@mui/system'; import { Theme } from '@mui/material/styles'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useEventCallback from '@mui/utils/useEventCallback'; -import { DateOrTimeView } from '../../../models'; import { useViews, UseViewsOptions } from '../useViews'; import type { UsePickerValueViewsResponse } from './usePickerValue.types'; import { isTimeView } from '../../utils/time-utils'; +import { DateOrTimeViewWithMeridiem } from '../../models'; -interface PickerViewsRendererBaseExternalProps +interface PickerViewsRendererBaseExternalProps extends Omit, 'openTo' | 'viewRenderers'> {} export type PickerViewsRendererProps< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends PickerViewsRendererBaseExternalProps, TAdditionalProps extends {}, > = TExternalProps & @@ -27,7 +27,7 @@ export type PickerViewsRendererProps< type PickerViewRenderer< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends PickerViewsRendererBaseExternalProps, TAdditionalProps extends {}, > = ( @@ -36,7 +36,7 @@ type PickerViewRenderer< export type PickerViewRendererLookup< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends PickerViewsRendererBaseExternalProps, TAdditionalProps extends {}, > = { @@ -48,7 +48,7 @@ export type PickerViewRendererLookup< */ export interface UsePickerViewsBaseProps< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UsePickerViewsProps, TAdditionalProps extends {}, > extends Omit, 'onChange' | 'onFocusedViewChange' | 'focusedView'> { @@ -80,7 +80,7 @@ export interface UsePickerViewsNonStaticProps { */ export interface UsePickerViewsProps< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UsePickerViewsProps, TAdditionalProps extends {}, > extends UsePickerViewsBaseProps, @@ -91,7 +91,7 @@ export interface UsePickerViewsProps< export interface UsePickerViewParams< TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UsePickerViewsProps, TAdditionalProps extends {}, > { @@ -102,7 +102,7 @@ export interface UsePickerViewParams< autoFocusView: boolean; } -export interface UsePickerViewsResponse { +export interface UsePickerViewsResponse { /** * Does the picker have at least one view that should be rendered in UI mode ? * If not, we can hide the icon to open the picker. @@ -113,7 +113,7 @@ export interface UsePickerViewsResponse { layoutProps: UsePickerViewsLayoutResponse; } -export interface UsePickerViewsLayoutResponse { +export interface UsePickerViewsLayoutResponse { view: TView | null; onViewChange: (view: TView) => void; views: readonly TView[]; @@ -127,7 +127,7 @@ export interface UsePickerViewsLayoutResponse { */ export const usePickerViews = < TValue, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UsePickerViewsProps, TAdditionalProps extends {}, >({ diff --git a/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx b/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx index 11d4ca2d00a0..7bd800892bc2 100644 --- a/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx @@ -6,7 +6,8 @@ import { usePicker } from '../usePicker'; import { LocalizationProvider } from '../../../LocalizationProvider'; import { PickersLayout } from '../../../PickersLayout'; import { DIALOG_WIDTH } from '../../constants/dimensions'; -import { FieldSection, DateOrTimeView } from '../../../models'; +import { FieldSection } from '../../../models'; +import { DateOrTimeViewWithMeridiem } from '../../models'; const PickerStaticLayout = styled(PickersLayout)(({ theme }) => ({ overflow: 'hidden', @@ -22,7 +23,7 @@ const PickerStaticLayout = styled(PickersLayout)(({ theme }) => ({ */ export const useStaticPicker = < TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseStaticPickerProps, >({ props, diff --git a/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.types.ts b/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.types.ts index c1525c5e4121..0abec5acb270 100644 --- a/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.types.ts @@ -7,13 +7,16 @@ import { BasePickerProps } from '../../models/props/basePickerProps'; import { UncapitalizeObjectKeys } from '../../utils/slots-migration'; import { UsePickerParams } from '../usePicker'; import { UsePickerViewsProps } from '../usePicker/usePickerViews'; -import { FieldSection, DateOrTimeView } from '../../../models'; +import { FieldSection } from '../../../models'; +import { DateOrTimeViewWithMeridiem } from '../../models'; -export interface UseStaticPickerSlotsComponent +export interface UseStaticPickerSlotsComponent extends ExportedPickersLayoutSlotsComponent {} -export interface UseStaticPickerSlotsComponentsProps - extends ExportedPickersLayoutSlotsComponentsProps {} +export interface UseStaticPickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem, +> extends ExportedPickersLayoutSlotsComponentsProps {} export interface StaticOnlyPickerProps { /** @@ -35,7 +38,7 @@ export interface StaticOnlyPickerProps { export interface UseStaticPickerProps< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, TExternalProps extends UsePickerViewsProps, > extends BasePickerProps, @@ -66,7 +69,7 @@ export interface UseStaticPickerProps< export interface UseStaticPickerParams< TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UseStaticPickerProps, > extends Pick< UsePickerParams, diff --git a/packages/x-date-pickers/src/internals/hooks/useViews.tsx b/packages/x-date-pickers/src/internals/hooks/useViews.tsx index 3e84cf2d5474..fc8733b69e9a 100644 --- a/packages/x-date-pickers/src/internals/hooks/useViews.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useViews.tsx @@ -2,22 +2,23 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { unstable_useControlled as useControlled } from '@mui/utils'; import type { PickerSelectionState } from './usePicker'; -import { DateOrTimeView } from '../../models'; import { MakeOptional } from '../models/helpers'; +import { DateOrTimeViewWithMeridiem } from '../models'; export type PickerOnChangeFn = ( date: TDate | null, selectionState?: PickerSelectionState, ) => void; -export interface UseViewsOptions { +export interface UseViewsOptions { /** * Callback fired when the value changes. * @template TValue * @param {TValue} value The new value. * @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete. + * @param {TView | undefined} selectedView Indicates the view in which the selection has been made. */ - onChange: (value: TValue, selectionState?: PickerSelectionState) => void; + onChange: (value: TValue, selectionState?: PickerSelectionState, selectedView?: TView) => void; /** * Callback fired on view change. * @template TView @@ -60,12 +61,12 @@ export interface UseViewsOptions { onFocusedViewChange?: (view: TView, hasFocus: boolean) => void; } -export interface ExportedUseViewsOptions +export interface ExportedUseViewsOptions extends MakeOptional, 'onChange'>, 'openTo' | 'views'> {} let warnedOnceNotValidView = false; -interface UseViewsResponse { +interface UseViewsResponse { view: TView; setView: (view: TView) => void; focusedView: TView | null; @@ -78,9 +79,10 @@ interface UseViewsResponse { value: TValue, currentViewSelectionState?: PickerSelectionState, ) => void; + setValueAndGoToView: (value: TValue, newView: TView | null, selectedView: TView) => void; } -export function useViews({ +export function useViews({ onChange, onViewChange, openTo, @@ -144,6 +146,9 @@ export function useViews({ const nextView: TView | null = views[viewIndex + 1] ?? null; const handleChangeView = useEventCallback((newView: TView) => { + if (newView === view) { + return; + } setView(newView); if (onViewChange) { @@ -151,19 +156,37 @@ export function useViews({ } }); + const handleFocusedViewChange = useEventCallback((viewToFocus: TView, hasFocus: boolean) => { + if (hasFocus) { + // Focus event + setFocusedView(viewToFocus); + } else { + // Blur event + setFocusedView( + (prevFocusedView) => (viewToFocus === prevFocusedView ? null : prevFocusedView), // If false the blur is due to view switching + ); + } + + onFocusedViewChange?.(viewToFocus, hasFocus); + }); + const goToNextView = useEventCallback(() => { if (nextView) { handleChangeView(nextView); } + handleFocusedViewChange(nextView, true); }); const setValueAndGoToNextView = useEventCallback( - (value: TValue, currentViewSelectionState?: PickerSelectionState) => { + (value: TValue, currentViewSelectionState?: PickerSelectionState, selectedView?: TView) => { const isSelectionFinishedOnCurrentView = currentViewSelectionState === 'finish'; + const hasMoreViews = selectedView + ? // handles case like `DateTimePicker`, where a view might return a `finish` selection state + // but we it's not the final view given all `views` -> overall selection state should be `partial`. + views.indexOf(selectedView) < views.length - 1 + : Boolean(nextView); const globalSelectionState = - isSelectionFinishedOnCurrentView && Boolean(nextView) - ? 'partial' - : currentViewSelectionState; + isSelectionFinishedOnCurrentView && hasMoreViews ? 'partial' : currentViewSelectionState; onChange(value, globalSelectionState); if (isSelectionFinishedOnCurrentView) { @@ -172,19 +195,15 @@ export function useViews({ }, ); - const handleFocusedViewChange = useEventCallback((viewToFocus: TView, hasFocus: boolean) => { - if (hasFocus) { - // Focus event - setFocusedView(viewToFocus); - } else { - // Blur event - setFocusedView( - (prevFocusedView) => (viewToFocus === prevFocusedView ? null : prevFocusedView), // If false the blur is due to view switching - ); - } - - onFocusedViewChange?.(viewToFocus, hasFocus); - }); + const setValueAndGoToView = useEventCallback( + (value: TValue, newView: TView | null, selectedView: TView) => { + onChange(value, newView ? 'partial' : 'finish', selectedView); + if (newView) { + handleChangeView(newView); + handleFocusedViewChange(newView, true); + } + }, + ); return { view, @@ -196,5 +215,6 @@ export function useViews({ defaultView: defaultView.current, goToNextView, setValueAndGoToNextView, + setValueAndGoToView, }; } diff --git a/packages/x-date-pickers/src/internals/models/common.ts b/packages/x-date-pickers/src/internals/models/common.ts index f95b803d7019..fed0d479c57e 100644 --- a/packages/x-date-pickers/src/internals/models/common.ts +++ b/packages/x-date-pickers/src/internals/models/common.ts @@ -1 +1,7 @@ +import { DateView, TimeView } from '@mui/x-date-pickers/models/views'; + export type WrapperVariant = 'mobile' | 'desktop' | null; + +export type TimeViewWithMeridiem = TimeView | 'meridiem'; + +export type DateOrTimeViewWithMeridiem = DateView | TimeViewWithMeridiem; diff --git a/packages/x-date-pickers/src/internals/models/index.ts b/packages/x-date-pickers/src/internals/models/index.ts index cbc6c8c464c5..7bb9a9cf63ac 100644 --- a/packages/x-date-pickers/src/internals/models/index.ts +++ b/packages/x-date-pickers/src/internals/models/index.ts @@ -1 +1,2 @@ export * from './fields'; +export * from './common'; diff --git a/packages/x-date-pickers/src/internals/models/props/basePickerProps.tsx b/packages/x-date-pickers/src/internals/models/props/basePickerProps.tsx index 4ca0934d2645..1fc745944af2 100644 --- a/packages/x-date-pickers/src/internals/models/props/basePickerProps.tsx +++ b/packages/x-date-pickers/src/internals/models/props/basePickerProps.tsx @@ -2,10 +2,10 @@ import * as React from 'react'; import { Theme } from '@mui/material/styles'; import { SxProps } from '@mui/system'; import { UsePickerBaseProps } from '../../hooks/usePicker'; -import { DateOrTimeView } from '../../../models'; import { PickersInputComponentLocaleText } from '../../../locales/utils/pickersLocaleTextApi'; import type { UsePickerViewsProps } from '../../hooks/usePicker/usePickerViews'; import { MakeOptional } from '../helpers'; +import { DateOrTimeViewWithMeridiem } from '../common'; /** * Props common to all pickers after applying the default props on each picker. @@ -13,7 +13,7 @@ import { MakeOptional } from '../helpers'; export interface BasePickerProps< TValue, TDate, - TView extends DateOrTimeView, + TView extends DateOrTimeViewWithMeridiem, TError, TExternalProps extends UsePickerViewsProps, TAdditionalProps extends {}, @@ -36,8 +36,12 @@ export interface BasePickerProps< /** * Props common to all pickers before applying the default props on each picker. */ -export interface BasePickerInputProps - extends Omit< +export interface BasePickerInputProps< + TValue, + TDate, + TView extends DateOrTimeViewWithMeridiem, + TError, +> extends Omit< MakeOptional, 'openTo' | 'views'>, 'viewRenderers' > {} diff --git a/packages/x-date-pickers/src/internals/models/props/clock.ts b/packages/x-date-pickers/src/internals/models/props/clock.ts new file mode 100644 index 000000000000..04a50a75da30 --- /dev/null +++ b/packages/x-date-pickers/src/internals/models/props/clock.ts @@ -0,0 +1,103 @@ +import { SxProps, Theme } from '@mui/material/styles'; +import { BaseTimeValidationProps, TimeValidationProps } from '../validation'; +import { PickerSelectionState } from '../../hooks/usePicker/usePickerValue.types'; +import { TimeStepOptions } from '../../../models'; +import type { ExportedDigitalClockProps } from '../../../DigitalClock/DigitalClock.types'; +import type { ExportedMultiSectionDigitalClockProps } from '../../../MultiSectionDigitalClock/MultiSectionDigitalClock.types'; +import type { ExportedUseViewsOptions } from '../../hooks/useViews'; +import { TimeViewWithMeridiem } from '../common'; + +export interface ExportedBaseClockProps + extends TimeValidationProps, + BaseTimeValidationProps { + /** + * 12h/24h view for hour selection clock. + * @default `utils.is12HourCycleInCurrentLocale()` + */ + ampm?: boolean; +} + +export interface BaseClockProps + extends ExportedUseViewsOptions, + ExportedBaseClockProps { + className?: string; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + /** + * The selected value. + * Used when the component is controlled. + */ + value?: TDate | null; + /** + * The default selected value. + * Used when the component is not controlled. + */ + defaultValue?: TDate | null; + /** + * Callback fired when the value changes. + * @template TDate + * @param {TDate | null} value The new value. + * @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete. + * @param {TView | undefined} selectedView Indicates the view in which the selection has been made. + */ + onChange?: ( + value: TDate | null, + selectionState?: PickerSelectionState, + selectedView?: TView, + ) => void; + /** + * If `true`, the picker views and text field are disabled. + * @default false + */ + disabled?: boolean; + /** + * If `true`, the picker views and text field are read-only. + * @default false + */ + readOnly?: boolean; +} + +export interface DesktopOnlyTimePickerProps + extends Omit, 'timeStep'>, + Omit, 'timeStep'> { + /** + * Amount of time options below or at which the single column time renderer is used. + * @default 24 + */ + thresholdToRenderTimeInASingleColumn?: number; + /** + * The time steps between two time unit options. + * For example, if `timeStep.minutes = 8`, then the available minute options will be `[0, 8, 16, 24, 32, 40, 48, 56]`. + * When single column time renderer is used, only `timeStep.minutes` will be used. + * @default{ hours: 1, minutes: 5, seconds: 5 } + */ + timeSteps?: TimeStepOptions; +} + +interface DigitalClockOnlyBaseProps { + /** + * If `true`, disabled digital clock items will not be rendered. + * @default false + */ + skipDisabled?: boolean; +} + +export interface DigitalClockOnlyProps extends DigitalClockOnlyBaseProps { + /** + * The time steps between two time options. + * For example, if `timeStep = 45`, then the available time options will be `[00:00, 00:45, 01:30, 02:15, 03:00, etc.]`. + * @default 30 + */ + timeStep?: number; +} + +export interface MultiSectionDigitalClockOnlyProps extends DigitalClockOnlyBaseProps { + /** + * The time steps between two time unit options. + * For example, if `timeStep.minutes = 8`, then the available minute options will be `[0, 8, 16, 24, 32, 40, 48, 56]`. + * @default{ hours: 1, minutes: 5, seconds: 5 } + */ + timeSteps?: TimeStepOptions; +} diff --git a/packages/x-date-pickers/src/internals/models/props/tabs.ts b/packages/x-date-pickers/src/internals/models/props/tabs.ts index 972b321c83c5..688dd69d4def 100644 --- a/packages/x-date-pickers/src/internals/models/props/tabs.ts +++ b/packages/x-date-pickers/src/internals/models/props/tabs.ts @@ -1,6 +1,6 @@ -import { DateOrTimeView } from '../../../models'; +import { DateOrTimeViewWithMeridiem } from '../common'; -export interface BaseTabsProps { +export interface BaseTabsProps { /** * Currently visible picker view. */ diff --git a/packages/x-date-pickers/src/internals/models/props/toolbar.ts b/packages/x-date-pickers/src/internals/models/props/toolbar.ts index 0c469c5e0de7..36275b5103a4 100644 --- a/packages/x-date-pickers/src/internals/models/props/toolbar.ts +++ b/packages/x-date-pickers/src/internals/models/props/toolbar.ts @@ -1,7 +1,7 @@ import * as React from 'react'; -import { DateOrTimeView } from '../../../models'; +import { DateOrTimeViewWithMeridiem } from '../common'; -export interface BaseToolbarProps +export interface BaseToolbarProps extends ExportedBaseToolbarProps { isLandscape: boolean; onChange: (newValue: TValue) => void; @@ -16,7 +16,7 @@ export interface BaseToolbarProps * @param {TView} view The view to open */ onViewChange: (view: TView) => void; - views: readonly DateOrTimeView[]; + views: readonly TView[]; disabled?: boolean; readOnly?: boolean; titleId?: string; diff --git a/packages/x-date-pickers/src/internals/utils/time-utils.ts b/packages/x-date-pickers/src/internals/utils/time-utils.ts index b58980322c7d..7710220e78ab 100644 --- a/packages/x-date-pickers/src/internals/utils/time-utils.ts +++ b/packages/x-date-pickers/src/internals/utils/time-utils.ts @@ -1,13 +1,15 @@ -import { DateOrTimeView, MuiPickersAdapter } from '../../models'; +import { MuiPickersAdapter } from '../../models'; +import { DateOrTimeViewWithMeridiem } from '../models'; -export const isTimeView = (view: DateOrTimeView) => ['hours', 'minutes', 'seconds'].includes(view); +const timeViews = ['hours', 'minutes', 'seconds']; +export const isTimeView = (view: DateOrTimeViewWithMeridiem) => timeViews.includes(view); -type Meridiem = 'am' | 'pm' | null; +export type Meridiem = 'am' | 'pm'; export const getMeridiem = ( date: TDate | null, utils: MuiPickersAdapter, -): Meridiem => { +): Meridiem | null => { if (!date) { return null; } @@ -15,7 +17,7 @@ export const getMeridiem = ( return utils.getHours(date) >= 12 ? 'pm' : 'am'; }; -export const convertValueToMeridiem = (value: number, meridiem: Meridiem, ampm: boolean) => { +export const convertValueToMeridiem = (value: number, meridiem: Meridiem | null, ampm: boolean) => { if (ampm) { const currentMeridiem = value >= 12 ? 'pm' : 'am'; if (currentMeridiem !== meridiem) { @@ -28,7 +30,7 @@ export const convertValueToMeridiem = (value: number, meridiem: Meridiem, ampm: export const convertToMeridiem = ( time: TDate, - meridiem: 'am' | 'pm', + meridiem: Meridiem, ampm: boolean, utils: MuiPickersAdapter, ) => { diff --git a/packages/x-date-pickers/src/internals/utils/views.ts b/packages/x-date-pickers/src/internals/utils/views.ts index 74d6d5b8b10d..4393c555fcc7 100644 --- a/packages/x-date-pickers/src/internals/utils/views.ts +++ b/packages/x-date-pickers/src/internals/utils/views.ts @@ -1,4 +1,5 @@ -import { DateOrTimeView, DateView } from '../../models'; +import { DateView } from '../../models'; +import { DateOrTimeViewWithMeridiem } from '../models'; export const isYearOnlyView = (views: readonly DateView[]): views is ReadonlyArray<'year'> => views.length === 1 && views[0] === 'year'; @@ -8,7 +9,7 @@ export const isYearAndMonthViews = ( ): views is ReadonlyArray<'month' | 'year'> => views.length === 2 && views.indexOf('month') !== -1 && views.indexOf('year') !== -1; -export const applyDefaultViewProps = ({ +export const applyDefaultViewProps = ({ openTo, defaultOpenTo, views, diff --git a/packages/x-date-pickers/src/locales/beBY.ts b/packages/x-date-pickers/src/locales/beBY.ts index a00641df6ed6..76004e920f7a 100644 --- a/packages/x-date-pickers/src/locales/beBY.ts +++ b/packages/x-date-pickers/src/locales/beBY.ts @@ -1,14 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { // maps TimeView to its translation hours: 'гадзіны', minutes: 'хвіліны', seconds: 'секунды', - // maps PickersToolbar["viewType"] to its translation - date: 'календара', - time: 'часу', + meridiem: 'мерыдыем', }; const beBYPickers: Partial> = { @@ -49,6 +48,9 @@ const beBYPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} хвілін`, secondsClockNumberText: (seconds) => `${seconds} секунд`, + // Digital clock labels + selectViewText: (view) => `Абярыце ${views[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Нумар тыдня', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/caES.ts b/packages/x-date-pickers/src/locales/caES.ts index 921767ba3f8d..4253843313b3 100644 --- a/packages/x-date-pickers/src/locales/caES.ts +++ b/packages/x-date-pickers/src/locales/caES.ts @@ -1,10 +1,12 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { hours: 'les hores', minutes: 'els minuts', seconds: 'els segons', + meridiem: 'meridiem', }; const caESPickers: Partial> = { @@ -47,6 +49,9 @@ const caESPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minuts`, secondsClockNumberText: (seconds) => `${seconds} segons`, + // Digital clock labels + selectViewText: (view) => `Seleccionar ${views[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Número de setmana', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/csCZ.ts b/packages/x-date-pickers/src/locales/csCZ.ts index 8c6f7788b557..35b44e4c8b02 100644 --- a/packages/x-date-pickers/src/locales/csCZ.ts +++ b/packages/x-date-pickers/src/locales/csCZ.ts @@ -1,13 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; - -// This object is not Partial because it is the default values +import { TimeViewWithMeridiem } from '../internals/models'; // maps TimeView to its translation -const timeViews = { +const timeViews: Record = { hours: 'Hodiny', minutes: 'Minuty', seconds: 'Sekundy', + meridiem: 'Odpoledne', }; const csCZPickers: Partial> = { @@ -48,6 +48,9 @@ const csCZPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minut`, secondsClockNumberText: (seconds) => `${seconds} sekund`, + // Digital clock labels + selectViewText: (view) => `Vyberte ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Týden v roce', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/daDK.ts b/packages/x-date-pickers/src/locales/daDK.ts index 1499c288f522..f46f0b28a797 100644 --- a/packages/x-date-pickers/src/locales/daDK.ts +++ b/packages/x-date-pickers/src/locales/daDK.ts @@ -1,11 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; // maps TimeView to its translation -const timeViews = { +const timeViews: Record = { hours: 'Timer', minutes: 'Minutter', seconds: 'Sekunder', + meridiem: 'Meridiem', }; const daDKPickers: Partial> = { @@ -48,6 +50,9 @@ const daDKPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minutter`, secondsClockNumberText: (seconds) => `${seconds} sekunder`, + // Digital clock labels + selectViewText: (view) => `Vælg ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Ugenummer', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/deDE.ts b/packages/x-date-pickers/src/locales/deDE.ts index 258782bba123..479b02455bd2 100644 --- a/packages/x-date-pickers/src/locales/deDE.ts +++ b/packages/x-date-pickers/src/locales/deDE.ts @@ -1,11 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; // maps TimeView to its translation -const timeViews = { +const timeViews: Record = { hours: 'Stunden', minutes: 'Minuten', seconds: 'Sekunden', + meridiem: 'Meridiem', }; const deDEPickers: Partial> = { @@ -48,6 +50,9 @@ const deDEPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} ${timeViews.minutes}`, secondsClockNumberText: (seconds) => `${seconds} ${timeViews.seconds}`, + // Digital clock labels + selectViewText: (view) => `Auswählen ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Kalenderwoche', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/enUS.ts b/packages/x-date-pickers/src/locales/enUS.ts index 34e94f533567..53fce05b2081 100644 --- a/packages/x-date-pickers/src/locales/enUS.ts +++ b/packages/x-date-pickers/src/locales/enUS.ts @@ -41,6 +41,9 @@ const enUSPickers: PickersLocaleText = { minutesClockNumberText: (minutes) => `${minutes} minutes`, secondsClockNumberText: (seconds) => `${seconds} seconds`, + // Digital clock labels + selectViewText: (view) => `Select ${view}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Week number', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/esES.ts b/packages/x-date-pickers/src/locales/esES.ts index fd28744db22b..b5fec0ff00d6 100644 --- a/packages/x-date-pickers/src/locales/esES.ts +++ b/packages/x-date-pickers/src/locales/esES.ts @@ -1,10 +1,12 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { hours: 'las horas', minutes: 'los minutos', seconds: 'los segundos', + meridiem: 'meridiano', }; const esESPickers: Partial> = { @@ -47,6 +49,9 @@ const esESPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minutos`, secondsClockNumberText: (seconds) => `${seconds} segundos`, + // Digital clock labels + selectViewText: (view) => `Seleccionar ${views[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Número de semana', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/faIR.ts b/packages/x-date-pickers/src/locales/faIR.ts index 29c6a753711e..5a748c92810d 100644 --- a/packages/x-date-pickers/src/locales/faIR.ts +++ b/packages/x-date-pickers/src/locales/faIR.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'ساعت ها', + minutes: 'دقیقه ها', + seconds: 'ثانیه ها', + meridiem: 'بعد از ظهر', +}; const faIRPickers: Partial> = { // Calendar navigation @@ -32,7 +40,7 @@ const faIRPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Select ${view}. ${ + ` را انتخاب کنید ${timeViews[view]}. ${ time === null ? 'هیچ ساعتی انتخاب نشده است' : `ساعت انتخاب ${adapter.format(time, 'fullTime')} می باشد` @@ -41,6 +49,9 @@ const faIRPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} دقیقه ها`, secondsClockNumberText: (seconds) => `${seconds} ثانیه ها`, + // Digital clock labels + selectViewText: (view) => ` را انتخاب کنید ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'عدد هفته', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/fiFI.ts b/packages/x-date-pickers/src/locales/fiFI.ts index 832d9b8a83c6..b4577defc315 100644 --- a/packages/x-date-pickers/src/locales/fiFI.ts +++ b/packages/x-date-pickers/src/locales/fiFI.ts @@ -1,10 +1,12 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { hours: 'tunnit', minutes: 'minuutit', seconds: 'sekuntit', + meridiem: 'iltapäivä', }; const fiFIPickers: Partial> = { @@ -45,6 +47,9 @@ const fiFIPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minuuttia`, secondsClockNumberText: (seconds) => `${seconds} sekunttia`, + // Digital clock labels + selectViewText: (view) => `Valitse ${views[view]}`, + // Calendar labels // calendarWeekNumberHeaderLabel: 'Week number', // calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/frFR.ts b/packages/x-date-pickers/src/locales/frFR.ts index 7bb1d96efbea..14ac7e7d7dbc 100644 --- a/packages/x-date-pickers/src/locales/frFR.ts +++ b/packages/x-date-pickers/src/locales/frFR.ts @@ -1,10 +1,12 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { hours: 'heures', minutes: 'minutes', seconds: 'secondes', + meridiem: 'méridien', }; const frFRPickers: Partial> = { @@ -47,6 +49,9 @@ const frFRPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minutes`, secondsClockNumberText: (seconds) => `${seconds} secondes`, + // Digital clock labels + selectViewText: (view) => `Choisir ${views[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Semaine', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/heIL.ts b/packages/x-date-pickers/src/locales/heIL.ts index 15ef5407e2ba..dceedbbff2b6 100644 --- a/packages/x-date-pickers/src/locales/heIL.ts +++ b/packages/x-date-pickers/src/locales/heIL.ts @@ -1,10 +1,12 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { hours: 'שעות', minutes: 'דקות', seconds: 'שניות', + meridiem: 'מרידיאם', }; const heILPickers: Partial> = { @@ -45,6 +47,9 @@ const heILPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} דקות`, secondsClockNumberText: (seconds) => `${seconds} שניות`, + // Digital clock labels + selectViewText: (view) => `בחירת ${views[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'שבוע מספר', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/huHU.ts b/packages/x-date-pickers/src/locales/huHU.ts index 5c122d550ad6..86fc0a23469c 100644 --- a/packages/x-date-pickers/src/locales/huHU.ts +++ b/packages/x-date-pickers/src/locales/huHU.ts @@ -1,11 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; // maps TimeView to its translation -const timeViews = { +const timeViews: Record = { hours: 'Óra', minutes: 'Perc', seconds: 'Másodperc', + meridiem: 'Délután', }; const huHUPickers: Partial> = { @@ -48,6 +50,9 @@ const huHUPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} ${timeViews.minutes.toLowerCase()}`, secondsClockNumberText: (seconds) => `${seconds} ${timeViews.seconds.toLowerCase()}`, + // Digital clock labels + selectViewText: (view) => `${timeViews[view]} kiválasztása`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Hét', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/isIS.ts b/packages/x-date-pickers/src/locales/isIS.ts index 087449dd16a0..161570338a8e 100644 --- a/packages/x-date-pickers/src/locales/isIS.ts +++ b/packages/x-date-pickers/src/locales/isIS.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'klukkustundir', + minutes: 'mínútur', + seconds: 'sekúndur', + meridiem: 'eftirmiðdagur', +}; const isISPickers: Partial> = { // Calendar navigation @@ -32,13 +40,16 @@ const isISPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Select ${view}. ${ + `Velja ${timeViews[view]}. ${ time === null ? 'Enginn tími valinn' : `Valinn tími er ${adapter.format(time, 'fullTime')}` }`, hoursClockNumberText: (hours) => `${hours} klukkustundir`, minutesClockNumberText: (minutes) => `${minutes} mínútur`, secondsClockNumberText: (seconds) => `${seconds} sekúndur`, + // Digital clock labels + selectViewText: (view) => `Velja ${timeViews[view]}`, + // Calendar labels // calendarWeekNumberHeaderLabel: 'Week number', // calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/itIT.ts b/packages/x-date-pickers/src/locales/itIT.ts index 110e89c959d6..feb675cc5fcc 100644 --- a/packages/x-date-pickers/src/locales/itIT.ts +++ b/packages/x-date-pickers/src/locales/itIT.ts @@ -1,10 +1,12 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { hours: 'le ore', minutes: 'i minuti', seconds: 'i secondi', + meridiem: 'il meridiano', }; const itITPickers: Partial> = { @@ -47,6 +49,9 @@ const itITPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minuti`, secondsClockNumberText: (seconds) => `${seconds} secondi`, + // Digital clock labels + selectViewText: (view) => `Seleziona ${views[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Numero settimana', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/jaJP.ts b/packages/x-date-pickers/src/locales/jaJP.ts index 1c27c1e73127..74aa23834109 100644 --- a/packages/x-date-pickers/src/locales/jaJP.ts +++ b/packages/x-date-pickers/src/locales/jaJP.ts @@ -1,11 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; // maps TimeView to its translation -const timeViews = { +const timeViews: Record = { hours: '時間', minutes: '分', seconds: '秒', + meridiem: 'メリディム', }; const jaJPPickers: Partial> = { @@ -48,6 +50,9 @@ const jaJPPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} ${timeViews.minutes}`, secondsClockNumberText: (seconds) => `${seconds} ${timeViews.seconds}`, + // Digital clock labels + selectViewText: (view) => `を選択 ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: '週番号', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/koKR.ts b/packages/x-date-pickers/src/locales/koKR.ts index 16987610632c..a44d3c218291 100644 --- a/packages/x-date-pickers/src/locales/koKR.ts +++ b/packages/x-date-pickers/src/locales/koKR.ts @@ -1,10 +1,12 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { hours: '시간을', minutes: '분을', seconds: '초를', + meridiem: '메리디엠', }; const koKRPickers: Partial> = { @@ -47,6 +49,9 @@ const koKRPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes}분`, secondsClockNumberText: (seconds) => `${seconds}초`, + // Digital clock labels + selectViewText: (view) => `${views[view]} 선택하기`, + // Calendar labels calendarWeekNumberHeaderLabel: '주 번호', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/kzKZ.ts b/packages/x-date-pickers/src/locales/kzKZ.ts index 50589f09fd34..bb43a39e2ae7 100644 --- a/packages/x-date-pickers/src/locales/kzKZ.ts +++ b/packages/x-date-pickers/src/locales/kzKZ.ts @@ -1,11 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; // Translation map for Clock Label -const timeViews = { +const timeViews: Record = { hours: 'Сағатты', minutes: 'Минутты', seconds: 'Секундты', + meridiem: 'Меридием', }; const kzKZPickers: Partial> = { @@ -46,6 +48,9 @@ const kzKZPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} минут`, secondsClockNumberText: (seconds) => `${seconds} секунд`, + // Digital clock labels + selectViewText: (view) => `${timeViews[view]} таңдау`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Апта нөмірі', calendarWeekNumberHeaderText: '№', diff --git a/packages/x-date-pickers/src/locales/nbNO.ts b/packages/x-date-pickers/src/locales/nbNO.ts index 178a34da0c96..d808a2d3fd55 100644 --- a/packages/x-date-pickers/src/locales/nbNO.ts +++ b/packages/x-date-pickers/src/locales/nbNO.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'timer', + minutes: 'minutter', + seconds: 'sekunder', + meridiem: 'meridiem', +}; const nbNOPickers: Partial> = { // Calendar navigation @@ -32,13 +40,16 @@ const nbNOPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Velg ${view}. ${ + `Velg ${timeViews[view]}. ${ time === null ? 'Ingen tid valgt' : `Valgt tid er ${adapter.format(time, 'fullTime')}` }`, hoursClockNumberText: (hours) => `${hours} timer`, minutesClockNumberText: (minutes) => `${minutes} minutter`, secondsClockNumberText: (seconds) => `${seconds} sekunder`, + // Digital clock labels + selectViewText: (view) => `Velg ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Ukenummer', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/nlNL.ts b/packages/x-date-pickers/src/locales/nlNL.ts index 53f0a2c8f10a..3d73af4c0595 100644 --- a/packages/x-date-pickers/src/locales/nlNL.ts +++ b/packages/x-date-pickers/src/locales/nlNL.ts @@ -1,6 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; -import { DateView } from '../models'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'uren', + minutes: 'minuten', + seconds: 'seconden', + meridiem: 'meridium', +}; const nlNLPickers: Partial> = { // Calendar navigation @@ -10,7 +17,7 @@ const nlNLPickers: Partial> = { // View navigation openPreviousView: 'open vorige view', openNextView: 'open volgende view', - calendarViewSwitchingButtonAriaLabel: (view: DateView) => + calendarViewSwitchingButtonAriaLabel: (view) => view === 'year' ? 'jaarweergave is geopend, schakel over naar kalenderweergave' : 'kalenderweergave is geopend, switch naar jaarweergave', @@ -33,7 +40,7 @@ const nlNLPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Selecteer ${view}. ${ + `Selecteer ${timeViews[view]}. ${ time === null ? 'Geen tijd geselecteerd' : `Geselecteerde tijd is ${adapter.format(time, 'fullTime')}` @@ -42,6 +49,9 @@ const nlNLPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minuten`, secondsClockNumberText: (seconds) => `${seconds} seconden`, + // Digital clock labels + selectViewText: (view) => `Selecteer ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Weeknummer', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/plPL.ts b/packages/x-date-pickers/src/locales/plPL.ts index e5b7cb8a1d98..6b7720e969c3 100644 --- a/packages/x-date-pickers/src/locales/plPL.ts +++ b/packages/x-date-pickers/src/locales/plPL.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'godzin', + minutes: 'minut', + seconds: 'sekund', + meridiem: 'popołudnie', +}; const plPLPickers: Partial> = { // Calendar navigation @@ -32,13 +40,16 @@ const plPLPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Select ${view}. ${ + `Wybierz ${timeViews[view]}. ${ time === null ? 'Nie wybrano czasu' : `Wybrany czas to ${adapter.format(time, 'fullTime')}` }`, hoursClockNumberText: (hours) => `${hours} godzin`, minutesClockNumberText: (minutes) => `${minutes} minut`, secondsClockNumberText: (seconds) => `${seconds} sekund`, + // Digital clock labels + selectViewText: (view) => `Wybierz ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Numer tygodnia', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/ptBR.ts b/packages/x-date-pickers/src/locales/ptBR.ts index 3f0da4834f45..032c8ccfccf0 100644 --- a/packages/x-date-pickers/src/locales/ptBR.ts +++ b/packages/x-date-pickers/src/locales/ptBR.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'horas', + minutes: 'minutos', + seconds: 'segundos', + meridiem: 'meridiano', +}; const ptBRPickers: Partial> = { // Calendar navigation @@ -32,7 +40,7 @@ const ptBRPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Selecione ${view}. ${ + `Selecione ${timeViews[view]}. ${ time === null ? 'Hora não selecionada' : `Selecionado a hora ${adapter.format(time, 'fullTime')}` @@ -41,6 +49,9 @@ const ptBRPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} minutos`, secondsClockNumberText: (seconds) => `${seconds} segundos`, + // Digital clock labels + selectViewText: (view) => `Selecione ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Número da semana', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/ruRU.ts b/packages/x-date-pickers/src/locales/ruRU.ts index d412bd7efa31..d947bcd5b253 100644 --- a/packages/x-date-pickers/src/locales/ruRU.ts +++ b/packages/x-date-pickers/src/locales/ruRU.ts @@ -1,11 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; // Translation map for Clock Label -const timeViews = { +const timeViews: Record = { hours: 'часы', minutes: 'минуты', seconds: 'секунды', + meridiem: 'меридием', }; const ruRUPickers: Partial> = { @@ -46,6 +48,9 @@ const ruRUPickers: Partial> = { minutesClockNumberText: (minutes) => `${minutes} минут`, secondsClockNumberText: (seconds) => `${seconds} секунд`, + // Digital clock labels + selectViewText: (view) => `Выбрать ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Номер недели', calendarWeekNumberHeaderText: '№', diff --git a/packages/x-date-pickers/src/locales/svSE.ts b/packages/x-date-pickers/src/locales/svSE.ts index ebcb693c8aa4..b8296507ce94 100644 --- a/packages/x-date-pickers/src/locales/svSE.ts +++ b/packages/x-date-pickers/src/locales/svSE.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'timmar', + minutes: 'minuter', + seconds: 'sekunder', + meridiem: 'meridiem', +}; const svSEPickers: Partial> = { // Calendar navigation @@ -32,13 +40,16 @@ const svSEPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Select ${view}. ${ + `Välj ${timeViews[view]}. ${ time === null ? 'Ingen tid vald' : `Vald tid är ${adapter.format(time, 'fullTime')}` }`, hoursClockNumberText: (hours) => `${hours} timmar`, minutesClockNumberText: (minutes) => `${minutes} minuter`, secondsClockNumberText: (seconds) => `${seconds} sekunder`, + // Digital clock labels + selectViewText: (view) => `Välj ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Vecka nummer', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/trTR.ts b/packages/x-date-pickers/src/locales/trTR.ts index d7a396ec0fc9..f7cdfe13dfd8 100644 --- a/packages/x-date-pickers/src/locales/trTR.ts +++ b/packages/x-date-pickers/src/locales/trTR.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'saat', + minutes: 'dakika', + seconds: 'saniye', + meridiem: 'öğleden sonra', +}; const trTRPickers: Partial> = { // Calendar navigation @@ -32,13 +40,16 @@ const trTRPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `${view} seç. ${ + `${timeViews[view]} seç. ${ time === null ? 'Zaman seçilmedi' : `Seçilen zaman: ${adapter.format(time, 'fullTime')}` }`, hoursClockNumberText: (hours) => `${hours} saat`, minutesClockNumberText: (minutes) => `${minutes} dakika`, secondsClockNumberText: (seconds) => `${seconds} saniye`, + // Digital clock labels + selectViewText: (view) => `Seç ${timeViews[view]}`, + // Calendar labels // calendarWeekNumberHeaderLabel: 'Week number', // calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/ukUA.ts b/packages/x-date-pickers/src/locales/ukUA.ts index de743fcec89b..dbfc9d42f966 100644 --- a/packages/x-date-pickers/src/locales/ukUA.ts +++ b/packages/x-date-pickers/src/locales/ukUA.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'годин', + minutes: 'хвилин', + seconds: 'секунд', + meridiem: 'Південь', +}; const ukUAPickers: Partial> = { // Calendar navigation @@ -32,13 +40,16 @@ const ukUAPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Select ${view}. ${ + `Вибрати ${timeViews[view]}. ${ time === null ? 'Час не вибраний' : `Вибрано час ${adapter.format(time, 'fullTime')}` }`, hoursClockNumberText: (hours) => `${hours} годин`, minutesClockNumberText: (minutes) => `${minutes} хвилин`, secondsClockNumberText: (seconds) => `${seconds} секунд`, + // Digital clock labels + selectViewText: (view) => `Вибрати ${timeViews[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: 'Номер тижня', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/locales/urPK.ts b/packages/x-date-pickers/src/locales/urPK.ts index ac2d77ef15e8..1c73d41e999b 100644 --- a/packages/x-date-pickers/src/locales/urPK.ts +++ b/packages/x-date-pickers/src/locales/urPK.ts @@ -1,5 +1,13 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; + +const timeViews: Record = { + hours: 'گھنٹے', + minutes: 'منٹ', + seconds: 'سیکنڈ', + meridiem: 'میریڈیم', +}; const urPKPickers: Partial> = { // Calendar navigation @@ -32,13 +40,16 @@ const urPKPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `${view} منتخب کریں ${ + `${timeViews[view]} منتخب کریں ${ time === null ? 'کوئی وقت منتخب نہیں' : `منتخب وقت ہے ${adapter.format(time, 'fullTime')}` }`, hoursClockNumberText: (hours) => `${hours} گھنٹے`, minutesClockNumberText: (minutes) => `${minutes} منٹ`, secondsClockNumberText: (seconds) => `${seconds} سیکنڈ`, + // Digital clock labels + selectViewText: (view) => `${timeViews[view]} منتخب کریں`, + // Calendar labels calendarWeekNumberHeaderLabel: 'ہفتہ نمبر', calendarWeekNumberHeaderText: 'نمبر', diff --git a/packages/x-date-pickers/src/locales/utils/pickersLocaleTextApi.ts b/packages/x-date-pickers/src/locales/utils/pickersLocaleTextApi.ts index 686f167a8eb0..a2acb4fa27bf 100644 --- a/packages/x-date-pickers/src/locales/utils/pickersLocaleTextApi.ts +++ b/packages/x-date-pickers/src/locales/utils/pickersLocaleTextApi.ts @@ -1,3 +1,4 @@ +import { TimeViewWithMeridiem } from '../../internals/models'; import { DateView, TimeView, MuiPickersAdapter, FieldSectionContentType } from '../../models'; export interface PickersComponentSpecificLocaleText { @@ -55,6 +56,9 @@ export interface PickersComponentAgnosticLocaleText { minutesClockNumberText: (minutes: string) => string; secondsClockNumberText: (seconds: string) => string; + // Digital clock labels + selectViewText: (view: TimeViewWithMeridiem) => string; + // Open picker labels openDatePickerDialogue: (date: TDate | null, utils: MuiPickersAdapter) => string; openTimePickerDialogue: (date: TDate | null, utils: MuiPickersAdapter) => string; diff --git a/packages/x-date-pickers/src/locales/zhCN.ts b/packages/x-date-pickers/src/locales/zhCN.ts index 8a7ac7da2d48..544b6d23c1bd 100644 --- a/packages/x-date-pickers/src/locales/zhCN.ts +++ b/packages/x-date-pickers/src/locales/zhCN.ts @@ -1,10 +1,12 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +import { TimeViewWithMeridiem } from '../internals/models'; -const views = { +const views: Record = { hours: '小时', minutes: '分钟', seconds: '秒', + meridiem: '子午线', }; const zhCNPickers: Partial> = { @@ -36,13 +38,16 @@ const zhCNPickers: Partial> = { // Clock labels clockLabelText: (view, time, adapter) => - `Select ${views[view]}. ${ + `选择 ${views[view]}. ${ time === null ? '未选择时间' : `已选择${adapter.format(time, 'fullTime')}` }`, hoursClockNumberText: (hours) => `${hours}小时`, minutesClockNumberText: (minutes) => `${minutes}分钟`, secondsClockNumberText: (seconds) => `${seconds}秒`, + // Digital clock labels + selectViewText: (view) => `选择 ${views[view]}`, + // Calendar labels calendarWeekNumberHeaderLabel: '周数', calendarWeekNumberHeaderText: '#', diff --git a/packages/x-date-pickers/src/models/common.ts b/packages/x-date-pickers/src/models/common.ts new file mode 100644 index 000000000000..902616712b71 --- /dev/null +++ b/packages/x-date-pickers/src/models/common.ts @@ -0,0 +1,5 @@ +export interface TimeStepOptions { + hours?: number; + minutes?: number; + seconds?: number; +} diff --git a/packages/x-date-pickers/src/models/index.ts b/packages/x-date-pickers/src/models/index.ts index 4a55f5ec3aea..340d121ff3c7 100644 --- a/packages/x-date-pickers/src/models/index.ts +++ b/packages/x-date-pickers/src/models/index.ts @@ -2,3 +2,4 @@ export * from './fields'; export * from './validation'; export * from './views'; export * from './adapters'; +export * from './common'; diff --git a/packages/x-date-pickers/src/tests/describe.types.ts b/packages/x-date-pickers/src/tests/describe.types.ts index c16c9c983482..3166ba6b727c 100644 --- a/packages/x-date-pickers/src/tests/describe.types.ts +++ b/packages/x-date-pickers/src/tests/describe.types.ts @@ -1 +1,8 @@ -export type PickerComponentFamily = 'picker' | 'field' | 'calendar' | 'clock' | 'static-picker'; +export type PickerComponentFamily = + | 'picker' + | 'field' + | 'calendar' + | 'clock' + | 'digital-clock' + | 'multi-section-digital-clock' + | 'static-picker'; diff --git a/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx b/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx index 2b1f7078b317..195f09ca9fc6 100644 --- a/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx +++ b/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import { screen, act, userEvent } from '@mui/monorepo/test/utils'; import { inputBaseClasses } from '@mui/material/InputBase'; +import { getExpectedOnChangeCount } from 'test/utils/pickers-utils'; import { DescribeValueOptions, DescribeValueTestSuite } from './describeValue.types'; export const testControlledUnControlled: DescribeValueTestSuite = ( @@ -20,6 +21,8 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( ...pickerParams } = getOptions(); + const params = pickerParams as DescribeValueOptions<'picker', any>; + describe('Controlled / uncontrolled value', () => { it('should render `props.defaultValue` if no `props.value` is passed', () => { render(); @@ -49,7 +52,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( assertRenderedValue(newValue); // TODO: Clean this exception or change the clock behavior - expect(onChange.callCount).to.equal(componentFamily === 'clock' ? 2 : 1); + expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily)); if (Array.isArray(newValue)) { newValue.forEach((value, index) => { expect(onChange.lastCall.args[0][index]).toEqualDateTime(value); @@ -65,7 +68,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( render(); const newValue = setNewValue(values[0]); - expect(onChange.callCount).to.equal(componentFamily === 'clock' ? 2 : 1); + expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily)); if (Array.isArray(newValue)) { newValue.forEach((value, index) => { expect(onChange.lastCall.args[0][index]).toEqualDateTime(value); @@ -97,10 +100,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); it('should not allow editing with keyboard in mobile pickers', () => { - if ( - componentFamily !== 'picker' || - (pickerParams as DescribeValueOptions<'picker', any>).variant !== 'mobile' - ) { + if (componentFamily !== 'picker' || params.variant !== 'mobile') { return; } const handleChange = spy(); @@ -117,10 +117,9 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); it('should have correct labelledby relationship when toolbar is shown', () => { - const params = pickerParams as DescribeValueOptions<'picker', any>; if ( componentFamily !== 'picker' || - (params.variant === 'desktop' && (params.type === 'time' || params.type === 'date-range')) + (params.variant === 'desktop' && params.type === 'date-range') ) { return; } @@ -136,10 +135,9 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); it('should have correct labelledby relationship with provided label when toolbar is hidden', () => { - const params = pickerParams as DescribeValueOptions<'picker', any>; if ( componentFamily !== 'picker' || - (params.variant === 'desktop' && (params.type === 'time' || params.type === 'date-range')) + (params.variant === 'desktop' && params.type === 'date-range') ) { return; } @@ -165,10 +163,9 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); it('should have correct labelledby relationship without label and hidden toolbar but external props', () => { - const params = pickerParams as DescribeValueOptions<'picker', any>; if ( componentFamily !== 'picker' || - (params.variant === 'desktop' && (params.type === 'time' || params.type === 'date-range')) + (params.variant === 'desktop' && params.type === 'date-range') ) { return; } diff --git a/packages/x-date-pickers/src/tests/describeValue/testPickerActionBar.tsx b/packages/x-date-pickers/src/tests/describeValue/testPickerActionBar.tsx index 11ac3cbd7644..e62a725995eb 100644 --- a/packages/x-date-pickers/src/tests/describeValue/testPickerActionBar.tsx +++ b/packages/x-date-pickers/src/tests/describeValue/testPickerActionBar.tsx @@ -9,17 +9,12 @@ export const testPickerActionBar: DescribeValueTestSuite = ( ElementToTest, getOptions, ) => { - const { componentFamily, render, values, emptyValue, setNewValue, variant, type } = getOptions(); + const { componentFamily, render, values, emptyValue, setNewValue, type } = getOptions(); if (componentFamily !== 'picker') { return; } - // No view to test - if (variant === 'desktop' && type === 'time') { - return; - } - describe('Picker action bar', () => { describe('clear action', () => { it('should call onClose, onChange with empty value and onAccept with empty value', () => { diff --git a/packages/x-date-pickers/src/tests/describeValue/testPickerOpenCloseLifeCycle.tsx b/packages/x-date-pickers/src/tests/describeValue/testPickerOpenCloseLifeCycle.tsx index 48f0d6ee5146..63efe7b35849 100644 --- a/packages/x-date-pickers/src/tests/describeValue/testPickerOpenCloseLifeCycle.tsx +++ b/packages/x-date-pickers/src/tests/describeValue/testPickerOpenCloseLifeCycle.tsx @@ -15,11 +15,6 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite return; } - // No view to test - if (pickerParams.variant === 'desktop' && pickerParams.type === 'time') { - return; - } - const viewWrapperRole = pickerParams.type === 'date-range' && pickerParams.variant === 'desktop' ? 'tooltip' : 'dialog'; diff --git a/packages/x-date-pickers/src/themeAugmentation/components.d.ts b/packages/x-date-pickers/src/themeAugmentation/components.d.ts index c567aacf537e..a394afa83a25 100644 --- a/packages/x-date-pickers/src/themeAugmentation/components.d.ts +++ b/packages/x-date-pickers/src/themeAugmentation/components.d.ts @@ -41,6 +41,10 @@ export interface PickerComponents { defaultProps?: ComponentsProps['MuiDayCalendarSkeleton']; styleOverrides?: ComponentsOverrides['MuiDayCalendarSkeleton']; }; + MuiDigitalClock?: { + defaultProps?: ComponentsProps['MuiDigitalClock']; + styleOverrides?: ComponentsOverrides['MuiDigitalClock']; + }; MuiLocalizationProvider?: { defaultProps?: ComponentsProps['MuiLocalizationProvider']; styleOverrides?: ComponentsOverrides['MuiLocalizationProvider']; @@ -49,6 +53,14 @@ export interface PickerComponents { defaultProps?: ComponentsProps['MuiMonthCalendar']; styleOverrides?: ComponentsOverrides['MuiMonthCalendar']; }; + MuiMultiSectionDigitalClock?: { + defaultProps?: ComponentsProps['MuiMultiSectionDigitalClock']; + styleOverrides?: ComponentsOverrides['MuiMultiSectionDigitalClock']; + }; + MuiMultiSectionDigitalClockSection?: { + defaultProps?: ComponentsProps['MuiMultiSectionDigitalClockSection']; + styleOverrides?: ComponentsOverrides['MuiMultiSectionDigitalClockSection']; + }; MuiPickersArrowSwitcher?: { defaultProps?: ComponentsProps['MuiPickersArrowSwitcher']; styleOverrides?: ComponentsOverrides['MuiPickersArrowSwitcher']; diff --git a/packages/x-date-pickers/src/themeAugmentation/overrides.d.ts b/packages/x-date-pickers/src/themeAugmentation/overrides.d.ts index 11601591d4f2..d250c8731c83 100644 --- a/packages/x-date-pickers/src/themeAugmentation/overrides.d.ts +++ b/packages/x-date-pickers/src/themeAugmentation/overrides.d.ts @@ -26,6 +26,11 @@ import { PickersToolbarClassKey, PickersToolbarTextClassKey, } from '../internals'; +import { DigitalClockClassKey } from '../DigitalClock'; +import { + MultiSectionDigitalClockClassKey, + MultiSectionDigitalClockSectionClassKey, +} from '../MultiSectionDigitalClock'; // prettier-ignore export interface PickersComponentNameToClassKey { @@ -39,8 +44,11 @@ export interface PickersComponentNameToClassKey { MuiDateTimePickerToolbar: DateTimePickerToolbarClassKey; MuiDayCalendar: DayCalendarClassKey; MuiDayCalendarSkeleton: DayCalendarSkeletonClassKey; + MuiDigitalClock: DigitalClockClassKey; MuiLocalizationProvider: never; MuiMonthCalendar: MonthCalendarClassKey; + MuiMultiSectionDigitalClock: MultiSectionDigitalClockClassKey; + MuiMultiSectionDigitalClockSection: MultiSectionDigitalClockSectionClassKey; MuiPickersArrowSwitcher: PickersArrowSwitcherClassKey; MuiPickersCalendarHeader: PickersCalendarHeaderClassKey; MuiPickersDay: PickersDayClassKey; diff --git a/packages/x-date-pickers/src/themeAugmentation/props.d.ts b/packages/x-date-pickers/src/themeAugmentation/props.d.ts index d15e448bb515..a75544b6a5f2 100644 --- a/packages/x-date-pickers/src/themeAugmentation/props.d.ts +++ b/packages/x-date-pickers/src/themeAugmentation/props.d.ts @@ -40,6 +40,11 @@ import { TimePickerProps, TimePickerToolbarProps } from '../TimePicker'; import { DesktopTimePickerProps } from '../DesktopTimePicker'; import { MobileTimePickerProps } from '../MobileTimePicker'; import { StaticTimePickerProps } from '../StaticTimePicker'; +import { ExportedDigitalClockProps } from '../DigitalClock'; +import { + ExportedMultiSectionDigitalClockSectionProps, + MultiSectionDigitalClockProps, +} from '../MultiSectionDigitalClock'; export interface PickersComponentsPropsList { MuiClock: ClockProps; @@ -52,8 +57,11 @@ export interface PickersComponentsPropsList { MuiDateTimePickerToolbar: DateTimePickerToolbarProps; MuiDayCalendar: DayCalendarProps; MuiDayCalendarSkeleton: DayCalendarSkeletonProps; + MuiDigitalClock: ExportedDigitalClockProps; MuiLocalizationProvider: LocalizationProviderProps; MuiMonthCalendar: MonthCalendarProps; + MuiMultiSectionDigitalClock: MultiSectionDigitalClockProps; + MuiMultiSectionDigitalClockSection: ExportedMultiSectionDigitalClockSectionProps; MuiPickersArrowSwitcher: ExportedPickersArrowSwitcherProps; MuiPickersCalendarHeader: ExportedCalendarHeaderProps; MuiPickersDay: PickersDayProps; diff --git a/packages/x-date-pickers/src/themeAugmentation/themeAugmentation.spec.ts b/packages/x-date-pickers/src/themeAugmentation/themeAugmentation.spec.ts index a70e0dc1eba7..6f32a6967c7f 100644 --- a/packages/x-date-pickers/src/themeAugmentation/themeAugmentation.spec.ts +++ b/packages/x-date-pickers/src/themeAugmentation/themeAugmentation.spec.ts @@ -22,6 +22,11 @@ import { import { pickersDayClasses } from '../PickersDay'; import { timePickerToolbarClasses } from '../TimePicker'; import { pickersMonthClasses } from '../MonthCalendar'; +import { digitalClockClasses } from '../DigitalClock'; +import { + multiSectionDigitalClockClasses, + multiSectionDigitalClockSectionClasses, +} from '../MultiSectionDigitalClock'; createTheme({ components: { @@ -63,6 +68,63 @@ createTheme({ }, }, }, + MuiDigitalClock: { + defaultProps: { + timeStep: 42, + // @ts-expect-error invalid MuiDigitalClock prop + someRandomProp: true, + }, + styleOverrides: { + root: { + backgroundColor: 'red', + [`.${digitalClockClasses.item}`]: { + backgroundColor: 'green', + }, + }, + // @ts-expect-error invalid MuiDigitalClock class key + content: { + backgroundColor: 'blue', + }, + }, + }, + MuiMultiSectionDigitalClock: { + defaultProps: { + timeSteps: { minutes: 42 }, + // @ts-expect-error invalid MuiMultiSectionDigitalClock prop + someRandomProp: true, + }, + styleOverrides: { + root: { + backgroundColor: 'red', + [`&.${multiSectionDigitalClockClasses.root}`]: { + backgroundColor: 'green', + }, + }, + // @ts-expect-error invalid MuiMultiSectionDigitalClock class key + content: { + backgroundColor: 'blue', + }, + }, + }, + MuiMultiSectionDigitalClockSection: { + defaultProps: { + className: 'class', + // @ts-expect-error invalid MuiMultiSectionDigitalClockSection prop + someRandomProp: true, + }, + styleOverrides: { + root: { + backgroundColor: 'red', + [`.${multiSectionDigitalClockSectionClasses.item}`]: { + backgroundColor: 'green', + }, + }, + // @ts-expect-error invalid MuiMultiSectionDigitalClockSection class key + content: { + backgroundColor: 'blue', + }, + }, + }, MuiClock: { defaultProps: { ampmInClock: true, diff --git a/packages/x-date-pickers/src/timeViewRenderers/index.ts b/packages/x-date-pickers/src/timeViewRenderers/index.ts index 618910380654..70b1b35e2c44 100644 --- a/packages/x-date-pickers/src/timeViewRenderers/index.ts +++ b/packages/x-date-pickers/src/timeViewRenderers/index.ts @@ -1,2 +1,6 @@ -export { renderTimeViewClock } from './timeViewRenderers'; +export { + renderTimeViewClock, + renderDigitalClockTimeView, + renderMultiSectionDigitalClockTimeView, +} from './timeViewRenderers'; export type { TimeViewRendererProps } from './timeViewRenderers'; diff --git a/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx b/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx index 0d903d64a3c7..5a1bc9963246 100644 --- a/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx +++ b/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx @@ -1,20 +1,30 @@ import * as React from 'react'; import { TimeClock, TimeClockProps } from '../TimeClock'; -import { DateOrTimeView, TimeView } from '../models'; +import { TimeView } from '../models'; +import { DigitalClock, DigitalClockProps } from '../DigitalClock'; +import { BaseClockProps } from '../internals/models/props/clock'; +import { + MultiSectionDigitalClock, + MultiSectionDigitalClockProps, +} from '../MultiSectionDigitalClock'; +import { isTimeView } from '../internals/utils/time-utils'; +import { TimeViewWithMeridiem } from '../internals/models'; +import type { TimePickerProps } from '../TimePicker/TimePicker.types'; -const isTimePickerView = (view: unknown): view is TimeView => - view === 'hours' || view === 'minutes' || view === 'seconds'; - -export interface TimeViewRendererProps - extends Omit, 'views' | 'openTo' | 'view' | 'onViewChange'> { +export type TimeViewRendererProps< + TView extends TimeViewWithMeridiem, + TComponentProps extends BaseClockProps, +> = Omit & { view: TView; onViewChange?: (view: TView) => void; views: readonly TView[]; -} +}; export const renderTimeViewClock = ({ view, onViewChange, + focusedView, + onFocusedViewChange, views, value, defaultValue, @@ -40,11 +50,13 @@ export const renderTimeViewClock = ({ autoFocus, showViewSwitcher, disableIgnoringDatePartForTimeValidation, -}: TimeViewRendererProps) => ( +}: TimeViewRendererProps>) => ( - view={view as TimeView} + view={view} onViewChange={onViewChange} - views={views.filter(isTimePickerView)} + focusedView={focusedView} + onFocusedViewChange={onFocusedViewChange} + views={views.filter(isTimeView)} value={value} defaultValue={defaultValue} onChange={onChange} @@ -71,3 +83,139 @@ export const renderTimeViewClock = ({ disableIgnoringDatePartForTimeValidation={disableIgnoringDatePartForTimeValidation} /> ); + +export const renderDigitalClockTimeView = ({ + view, + onViewChange, + focusedView, + onFocusedViewChange, + views, + value, + defaultValue, + onChange, + className, + classes, + disableFuture, + disablePast, + minTime, + maxTime, + shouldDisableTime, + shouldDisableClock, + minutesStep, + ampm, + components, + componentsProps, + slots, + slotProps, + readOnly, + disabled, + sx, + autoFocus, + disableIgnoringDatePartForTimeValidation, + timeSteps, + skipDisabled, +}: TimeViewRendererProps< + Extract, + Omit, 'timeStep'> & Pick, 'timeSteps'> +>) => ( + + view={view} + onViewChange={onViewChange} + focusedView={focusedView} + onFocusedViewChange={onFocusedViewChange} + views={views.filter(isTimeView)} + value={value} + defaultValue={defaultValue} + onChange={onChange} + className={className} + classes={classes} + disableFuture={disableFuture} + disablePast={disablePast} + minTime={minTime} + maxTime={maxTime} + shouldDisableTime={shouldDisableTime} + shouldDisableClock={shouldDisableClock} + minutesStep={minutesStep} + ampm={ampm} + components={components} + componentsProps={componentsProps} + slots={slots} + slotProps={slotProps} + readOnly={readOnly} + disabled={disabled} + sx={sx} + autoFocus={autoFocus} + disableIgnoringDatePartForTimeValidation={disableIgnoringDatePartForTimeValidation} + timeStep={timeSteps?.minutes} + skipDisabled={skipDisabled} + /> +); + +export const renderMultiSectionDigitalClockTimeView = ({ + view, + onViewChange, + focusedView, + onFocusedViewChange, + views, + value, + defaultValue, + onChange, + className, + classes, + disableFuture, + disablePast, + minTime, + maxTime, + shouldDisableTime, + shouldDisableClock, + minutesStep, + ampm, + components, + componentsProps, + slots, + slotProps, + readOnly, + disabled, + sx, + autoFocus, + disableIgnoringDatePartForTimeValidation, + timeSteps, + skipDisabled, +}: TimeViewRendererProps< + TimeViewWithMeridiem, + Omit, 'timeSteps'> & + Omit, 'timeStep'> & + Pick, 'timeSteps'> +>) => ( + + view={view} + onViewChange={onViewChange} + focusedView={focusedView} + onFocusedViewChange={onFocusedViewChange} + views={views.filter(isTimeView)} + value={value} + defaultValue={defaultValue} + onChange={onChange} + className={className} + classes={classes} + disableFuture={disableFuture} + disablePast={disablePast} + minTime={minTime} + maxTime={maxTime} + shouldDisableTime={shouldDisableTime} + shouldDisableClock={shouldDisableClock} + minutesStep={minutesStep} + ampm={ampm} + components={components} + componentsProps={componentsProps} + slots={slots} + slotProps={slotProps} + readOnly={readOnly} + disabled={disabled} + sx={sx} + autoFocus={autoFocus} + disableIgnoringDatePartForTimeValidation={disableIgnoringDatePartForTimeValidation} + timeSteps={timeSteps} + skipDisabled={skipDisabled} + /> +); diff --git a/scripts/x-date-pickers-pro.exports.json b/scripts/x-date-pickers-pro.exports.json index 7c4ba1dbd6df..51d12ba955e8 100644 --- a/scripts/x-date-pickers-pro.exports.json +++ b/scripts/x-date-pickers-pro.exports.json @@ -109,9 +109,18 @@ { "name": "DesktopTimePickerProps", "kind": "Interface" }, { "name": "DesktopTimePickerSlotsComponent", "kind": "Interface" }, { "name": "DesktopTimePickerSlotsComponentsProps", "kind": "Interface" }, + { "name": "DigitalClock", "kind": "Variable" }, + { "name": "digitalClockClasses", "kind": "Variable" }, + { "name": "DigitalClockClasses", "kind": "Interface" }, + { "name": "DigitalClockClassKey", "kind": "TypeAlias" }, + { "name": "DigitalClockProps", "kind": "Interface" }, + { "name": "DigitalClockSlotsComponent", "kind": "Interface" }, + { "name": "DigitalClockSlotsComponentsProps", "kind": "Interface" }, { "name": "enUS", "kind": "Variable" }, { "name": "esES", "kind": "Variable" }, { "name": "ExportedDateRangeCalendarProps", "kind": "Interface" }, + { "name": "ExportedDigitalClockProps", "kind": "Interface" }, + { "name": "ExportedMultiSectionDigitalClockSectionProps", "kind": "Interface" }, { "name": "ExportedPickersLayoutSlotsComponent", "kind": "Interface" }, { "name": "ExportedPickersLayoutSlotsComponentsProps", "kind": "Interface" }, { "name": "ExportedPickersMonthProps", "kind": "Interface" }, @@ -132,7 +141,9 @@ { "name": "getDateRangePickerDayUtilityClass", "kind": "Function" }, { "name": "getDateRangePickerToolbarUtilityClass", "kind": "Function" }, { "name": "getDayCalendarSkeletonUtilityClass", "kind": "Variable" }, + { "name": "getDigitalClockUtilityClass", "kind": "Function" }, { "name": "getMonthCalendarUtilityClass", "kind": "Function" }, + { "name": "getMultiSectionDigitalClockUtilityClass", "kind": "Function" }, { "name": "getPickersDayUtilityClass", "kind": "Function" }, { "name": "getTimeClockUtilityClass", "kind": "Function" }, { "name": "getYearCalendarUtilityClass", "kind": "Function" }, @@ -177,6 +188,16 @@ { "name": "MultiInputFieldSlotTextFieldProps", "kind": "Interface" }, { "name": "MultiInputTimeRangeField", "kind": "Variable" }, { "name": "MultiInputTimeRangeFieldProps", "kind": "Interface" }, + { "name": "MultiSectionDigitalClock", "kind": "Variable" }, + { "name": "multiSectionDigitalClockClasses", "kind": "Variable" }, + { "name": "MultiSectionDigitalClockClasses", "kind": "Interface" }, + { "name": "MultiSectionDigitalClockClassKey", "kind": "TypeAlias" }, + { "name": "MultiSectionDigitalClockProps", "kind": "Interface" }, + { "name": "multiSectionDigitalClockSectionClasses", "kind": "Variable" }, + { "name": "MultiSectionDigitalClockSectionClasses", "kind": "Interface" }, + { "name": "MultiSectionDigitalClockSectionClassKey", "kind": "TypeAlias" }, + { "name": "MultiSectionDigitalClockSlotsComponent", "kind": "Interface" }, + { "name": "MultiSectionDigitalClockSlotsComponentsProps", "kind": "Interface" }, { "name": "nbNO", "kind": "Variable" }, { "name": "nlNL", "kind": "Variable" }, { "name": "PickersActionBar", "kind": "Function" }, @@ -226,6 +247,8 @@ { "name": "RangePosition", "kind": "TypeAlias" }, { "name": "renderDateRangeViewCalendar", "kind": "Variable" }, { "name": "renderDateViewCalendar", "kind": "Variable" }, + { "name": "renderDigitalClockTimeView", "kind": "Variable" }, + { "name": "renderMultiSectionDigitalClockTimeView", "kind": "Variable" }, { "name": "renderTimeViewClock", "kind": "Variable" }, { "name": "ruRU", "kind": "Variable" }, { "name": "SingleInputDateRangeField", "kind": "Variable" }, @@ -270,9 +293,10 @@ { "name": "TimePickerToolbarClassKey", "kind": "TypeAlias" }, { "name": "TimePickerToolbarProps", "kind": "Interface" }, { "name": "TimeRangeValidationError", "kind": "TypeAlias" }, + { "name": "TimeStepOptions", "kind": "Interface" }, { "name": "TimeValidationError", "kind": "TypeAlias" }, { "name": "TimeView", "kind": "TypeAlias" }, - { "name": "TimeViewRendererProps", "kind": "Interface" }, + { "name": "TimeViewRendererProps", "kind": "TypeAlias" }, { "name": "trTR", "kind": "Variable" }, { "name": "ukUA", "kind": "Variable" }, { "name": "unstable_useDateField", "kind": "Variable" }, diff --git a/scripts/x-date-pickers.exports.json b/scripts/x-date-pickers.exports.json index e5f0f0379752..c21243e7c87f 100644 --- a/scripts/x-date-pickers.exports.json +++ b/scripts/x-date-pickers.exports.json @@ -79,8 +79,17 @@ { "name": "DesktopTimePickerProps", "kind": "Interface" }, { "name": "DesktopTimePickerSlotsComponent", "kind": "Interface" }, { "name": "DesktopTimePickerSlotsComponentsProps", "kind": "Interface" }, + { "name": "DigitalClock", "kind": "Variable" }, + { "name": "digitalClockClasses", "kind": "Variable" }, + { "name": "DigitalClockClasses", "kind": "Interface" }, + { "name": "DigitalClockClassKey", "kind": "TypeAlias" }, + { "name": "DigitalClockProps", "kind": "Interface" }, + { "name": "DigitalClockSlotsComponent", "kind": "Interface" }, + { "name": "DigitalClockSlotsComponentsProps", "kind": "Interface" }, { "name": "enUS", "kind": "Variable" }, { "name": "esES", "kind": "Variable" }, + { "name": "ExportedDigitalClockProps", "kind": "Interface" }, + { "name": "ExportedMultiSectionDigitalClockSectionProps", "kind": "Interface" }, { "name": "ExportedPickersLayoutSlotsComponent", "kind": "Interface" }, { "name": "ExportedPickersLayoutSlotsComponentsProps", "kind": "Interface" }, { "name": "ExportedPickersMonthProps", "kind": "Interface" }, @@ -98,7 +107,9 @@ { "name": "frFR", "kind": "Variable" }, { "name": "getDateCalendarUtilityClass", "kind": "Variable" }, { "name": "getDayCalendarSkeletonUtilityClass", "kind": "Variable" }, + { "name": "getDigitalClockUtilityClass", "kind": "Function" }, { "name": "getMonthCalendarUtilityClass", "kind": "Function" }, + { "name": "getMultiSectionDigitalClockUtilityClass", "kind": "Function" }, { "name": "getPickersDayUtilityClass", "kind": "Function" }, { "name": "getTimeClockUtilityClass", "kind": "Function" }, { "name": "getYearCalendarUtilityClass", "kind": "Function" }, @@ -131,6 +142,16 @@ { "name": "MonthCalendarProps", "kind": "Interface" }, { "name": "MuiPickersAdapter", "kind": "Interface" }, { "name": "MuiPickersAdapterContext", "kind": "Variable" }, + { "name": "MultiSectionDigitalClock", "kind": "Variable" }, + { "name": "multiSectionDigitalClockClasses", "kind": "Variable" }, + { "name": "MultiSectionDigitalClockClasses", "kind": "Interface" }, + { "name": "MultiSectionDigitalClockClassKey", "kind": "TypeAlias" }, + { "name": "MultiSectionDigitalClockProps", "kind": "Interface" }, + { "name": "multiSectionDigitalClockSectionClasses", "kind": "Variable" }, + { "name": "MultiSectionDigitalClockSectionClasses", "kind": "Interface" }, + { "name": "MultiSectionDigitalClockSectionClassKey", "kind": "TypeAlias" }, + { "name": "MultiSectionDigitalClockSlotsComponent", "kind": "Interface" }, + { "name": "MultiSectionDigitalClockSlotsComponentsProps", "kind": "Interface" }, { "name": "nbNO", "kind": "Variable" }, { "name": "nlNL", "kind": "Variable" }, { "name": "PickersActionBar", "kind": "Function" }, @@ -177,6 +198,8 @@ { "name": "plPL", "kind": "Variable" }, { "name": "ptBR", "kind": "Variable" }, { "name": "renderDateViewCalendar", "kind": "Variable" }, + { "name": "renderDigitalClockTimeView", "kind": "Variable" }, + { "name": "renderMultiSectionDigitalClockTimeView", "kind": "Variable" }, { "name": "renderTimeViewClock", "kind": "Variable" }, { "name": "ruRU", "kind": "Variable" }, { "name": "StaticDatePicker", "kind": "Variable" }, @@ -210,9 +233,10 @@ { "name": "TimePickerToolbarClasses", "kind": "Interface" }, { "name": "TimePickerToolbarClassKey", "kind": "TypeAlias" }, { "name": "TimePickerToolbarProps", "kind": "Interface" }, + { "name": "TimeStepOptions", "kind": "Interface" }, { "name": "TimeValidationError", "kind": "TypeAlias" }, { "name": "TimeView", "kind": "TypeAlias" }, - { "name": "TimeViewRendererProps", "kind": "Interface" }, + { "name": "TimeViewRendererProps", "kind": "TypeAlias" }, { "name": "trTR", "kind": "Variable" }, { "name": "ukUA", "kind": "Variable" }, { "name": "unstable_useDateField", "kind": "Variable" }, diff --git a/test/utils/pickers-utils.tsx b/test/utils/pickers-utils.tsx index dc2877f37ee1..9a7ed49af244 100644 --- a/test/utils/pickers-utils.tsx +++ b/test/utils/pickers-utils.tsx @@ -25,6 +25,7 @@ import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalali'; import { MuiPickersAdapter } from '@mui/x-date-pickers/models'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { CLOCK_WIDTH } from '@mui/x-date-pickers/TimeClock/shared'; +import { PickerComponentFamily } from '@mui/x-date-pickers/tests/describe.types'; export type AdapterName = | 'date-fns' @@ -546,3 +547,14 @@ export class MockedDataTransfer implements DataTransfer { this.yOffset = yOffset; } } + +export const getExpectedOnChangeCount = (componentFamily: PickerComponentFamily) => { + switch (componentFamily) { + case 'clock': + return 2; + case 'multi-section-digital-clock': + return 3; + default: + return 1; + } +};