Skip to content

Commit

Permalink
Merge pull request #140 from osstotalsoft/feature/DateTimeUpdate
Browse files Browse the repository at this point in the history
update packages and refactor DateTime component
  • Loading branch information
DCosti authored Nov 19, 2024
2 parents 5f2804c + a98b457 commit 4778290
Show file tree
Hide file tree
Showing 17 changed files with 245 additions and 512 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@
"@mui/material": "^6.1.6",
"@mui/system": "^6.1.6",
"@mui/types": "^7.2.19",
"@mui/x-date-pickers": "5.0.20",
"@mui/x-date-pickers": "^7.22.2",
"attr-accept": "^2.2.5",
"chart.js": "4.4.6",
"classnames": "^2.5.1",
"date-fns": "^2.30.0",
"date-fns": "^4.1.0",
"i18next": "^23.16.5",
"lodash": "^4.17.21",
"ramda": "^0.30.1",
Expand Down
18 changes: 9 additions & 9 deletions src/components/inputs/DateTime/DateTime.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import DateTime from './DateTime'
import { render, userClick } from 'testingUtils'
import DateTime from './DateTime'

const value = '2022-03-14 16:35:25.123'
const value = new Date('2022-03-14 16:35:25.123')

describe('Standard Date Picker', () => {
it('renders a Date component by default', () => {
Expand Down Expand Up @@ -48,7 +48,7 @@ describe('Standard Date Picker', () => {
describe('Date Picker Formats', () => {
it('renders the date in french', () => {
// arrange
render(<DateTime showPicker="dateTime" value={value} format="fr" />)
render(<DateTime showPicker="dateTime" value={value} localeFormat="fr" />)

// act
const pickers = screen.getAllByDisplayValue('14/03/2022 16:35')
Expand All @@ -59,18 +59,18 @@ describe('Date Picker Formats', () => {

it('renders the date in united states', () => {
// arrange
render(<DateTime showPicker="dateTime" value={value} format="en-US" />)
render(<DateTime showPicker="dateTime" value={value} localeFormat="en-US" />)

// act
const picker = screen.getAllByDisplayValue('03/14/2022 04:35 pm')
const picker = screen.getAllByDisplayValue('03/14/2022 04:35 PM')

// assert
expect(picker).toHaveLength(1)
})

it('renders the date in romanian', () => {
// arrange
render(<DateTime showPicker="dateTime" value={value} format="ro" />)
render(<DateTime showPicker="dateTime" value={value} localeFormat="ro" />)

// act
const picker = screen.getAllByDisplayValue('14.03.2022 16:35')
Expand All @@ -86,19 +86,19 @@ describe('Date Time buttons work', () => {
render(<DateTime value={value} isClearable={true} />)

// act
await waitFor(() => fireEvent.click(screen.getByLabelText('Clear')))
await waitFor(() => fireEvent.click(screen.getByTitle('Clear')))

// assert
expect(() => screen.getAllByDisplayValue('14.03.2022')).toThrow()
})

it('opens dialog to choose date', async () => {
// arrange
render(<DateTime value={value} />)
render(<DateTime value={null} />)
expect(() => screen.getByRole('dialog')).toThrow()

// act
await waitFor(() => fireEvent.click(screen.getByLabelText('Open')))
await waitFor(() => fireEvent.click(screen.getByLabelText('Choose date')))

// assert
expect(() => screen.getByRole('dialog')).not.toThrow()
Expand Down
264 changes: 86 additions & 178 deletions src/components/inputs/DateTime/DateTime.tsx
Original file line number Diff line number Diff line change
@@ -1,166 +1,97 @@
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'
import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import {
TimePicker,
LocalizationProvider,
DatePicker,
DatePickerProps,
DatePickerSlotProps,
DatePickerSlots,
DateTimePicker,
DateTimePickerProps,
DateTimePickerSlotProps,
DateTimePickerSlots,
LocalizationProvider,
TimePicker,
TimePickerProps,
DatePickerProps
TimePickerSlotProps,
TimePickerSlots
} from '@mui/x-date-pickers'
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'
import { CalendarTodaySmallIcon } from './DateTimeStyles'
import * as R from 'ramda'
import ro from 'date-fns/locale/ro'
import fr from 'date-fns/locale/fr'
import ru from 'date-fns/locale/ru'
import de from 'date-fns/locale/de'
import enUS from 'date-fns/locale/en-US'
import DateTimeEndAdornment from './DateTimeEndAdornment'
import TextField, { TextFieldProps } from '../TextField'
import { T, DateTimeProps, CustomSlotsComponent } from './types'
import { SvgIconComponent } from '@mui/icons-material'
import { DateTimePickerSlotsComponent } from '@mui/x-date-pickers/DateTimePicker/DateTimePicker'
import { TimePickerSlotsComponent } from '@mui/x-date-pickers/TimePicker/TimePicker'
import { DatePickerSlotsComponent } from '@mui/x-date-pickers/DatePicker/DatePicker'
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3'
import { de, enUS, fr, ro } from 'date-fns/locale'
import { DateTimeProps } from './types'
import { cond, equals } from 'ramda'

const localeMap = {
de: de,
['en-US']: enUS,
fr: fr,
ro: ro,
ru: ru
}
const localeMap = { de, ['en-US']: enUS, fr, ro }

const defaultComponents = {
OpenPickerIcon: CalendarTodaySmallIcon
} as CustomSlotsComponent

const DateTime: React.FC<DateTimeProps> = ({
const DateTime: React.FC<DateTimeProps<Date, string>> = ({
dateAdapter = AdapterDateFns,
adapterLocale,
value,
onChange,
isClearable = false,
showPicker = 'date',
components,
inputProps,
format = 'ro',
open: origOpen = false,
onClose,
value: origValue = null,
onChange: origOnChange,
isClearable,
required,
disabled,
error,
slotProps = {},
slots = {},
localeFormat = 'ro',
helperText,
error,
...rest
}) => {
// Code to serve the "Open/Close" functionality
const [open, setOpen] = useState(origOpen)
useLayoutEffect(() => {
setOpen(origOpen)
}, [origOpen])

const handleOpen = useCallback(() => {
setOpen(true)
}, [])
const handleClose = useCallback(() => {
setOpen(false)
if (onClose) onClose()
}, [onClose])
// ---------------------------------------------

// Code to serve the "Clearable" functionality
const [value, setValue] = useState(origValue)
useLayoutEffect(() => {
setValue(origValue)
}, [origValue])
const handleChange = useCallback(
(value: T) => {
const changeValue = origOnChange ?? setValue
changeValue(value)
},
[origOnChange]
const locale = useMemo(() => adapterLocale ?? localeMap[localeFormat] ?? ro, [adapterLocale, localeFormat])
const commonSlotProps = useMemo(
() => ({
...slotProps,
field: { ...slotProps?.field, clearable: isClearable },
textField: { ...slotProps?.textField, helperText, error }
}),
[error, helperText, isClearable, slotProps]
)
const internalIsClearable = useMemo(() => Boolean(isClearable) && Boolean(value), [isClearable, value])
const handleClear = useCallback(() => {
handleChange(null)
}, [handleChange])
// ---------------------------------------------

const mergedComponents = useMemo(() => R.mergeRight(defaultComponents, components), [components])

const renderInput = useCallback(
(params: Partial<TextFieldProps>) => {
const OpenPickerIcon = mergedComponents.OpenPickerIcon as SvgIconComponent
return (
<TextField
fullWidth
{...inputProps}
{...params}
required={required}
error={error}
helperText={helperText}
endAdornment={
<DateTimeEndAdornment
isClearable={internalIsClearable}
onClear={handleClear}
onOpen={handleOpen}
OpenPickerIcon={OpenPickerIcon}
disabled={disabled}
const renderPicker = useMemo(
() =>
cond([
[
equals('date'),
() => (
<DatePicker
value={value}
onChange={onChange}
slotProps={commonSlotProps as DatePickerSlotProps<Date, false>}
slots={slots as DatePickerSlots<Date>}
{...(rest as DatePickerProps<Date>)}
/>
)
],
[
equals('dateTime'),
() => (
<DateTimePicker
value={value}
onChange={onChange}
slotProps={commonSlotProps as DateTimePickerSlotProps<Date, false>}
slots={slots as DateTimePickerSlots<Date>}
{...(rest as DateTimePickerProps<Date>)}
/>
)
],
[
equals('time'),
() => (
<TimePicker
value={value}
onChange={onChange}
slotProps={commonSlotProps as TimePickerSlotProps<Date, false>}
slots={slots as TimePickerSlots<Date>}
{...(rest as TimePickerProps<Date>)}
/>
}
/>
)
},
[
disabled,
error,
handleClear,
handleOpen,
helperText,
inputProps,
required,
internalIsClearable,
mergedComponents.OpenPickerIcon
]
)
]
])(showPicker),
[commonSlotProps, onChange, rest, showPicker, slots, value]
)

const localeUsed = useMemo(() => localeMap[format] ?? adapterLocale ?? localeMap.ro, [format, adapterLocale])

const commonProps = { renderInput, open, onClose: handleClose, value, onChange: handleChange, disabled }
const renderPicker = () => {
switch (showPicker) {
case 'dateTime':
return (
<DateTimePicker
components={mergedComponents as Partial<DateTimePickerSlotsComponent & CustomSlotsComponent>}
{...commonProps}
{...(rest as DateTimePickerProps<T, T>)}
/>
)
case 'time':
return (
<TimePicker
components={mergedComponents as Partial<TimePickerSlotsComponent & CustomSlotsComponent>}
{...commonProps}
{...(rest as TimePickerProps<T, T>)}
/>
)

default:
return (
<DatePicker
components={mergedComponents as Partial<DatePickerSlotsComponent & CustomSlotsComponent>}
{...commonProps}
{...(rest as DatePickerProps<T, T>)}
/>
)
}
}
return (
<LocalizationProvider dateAdapter={dateAdapter} adapterLocale={localeUsed}>
{renderPicker()}
<LocalizationProvider dateAdapter={dateAdapter} adapterLocale={locale}>
{renderPicker}
</LocalizationProvider>
)
}
Expand All @@ -177,58 +108,35 @@ DateTime.propTypes = {
* The adapterLocale object/string from the engine you use for displaying the date
*/
adapterLocale: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
/**
* Choose the type of picker you want displayed by the component
* @default 'date'
*/
showPicker: PropTypes.oneOf(['date', 'dateTime', 'time']),
/**
* The components used for each slot. Either a string to use a HTML element or a component.
*/
components: PropTypes.shape({
LeftArrowButton: PropTypes.any,
LeftArrowIcon: PropTypes.any,
OpenPickerIcon: PropTypes.any,
RightArrowButton: PropTypes.any,
RightArrowIcon: PropTypes.any,
SwitchViewButton: PropTypes.any,
SwitchViewIcon: PropTypes.any
}),
/**
* Properties that will be passed to the rendered input. This is a TextField.
*/
inputProps: PropTypes.object,
/**
* A small sample of ISO format names that will be used to display the date.
* @default 'ro'
*/
format: PropTypes.oneOf(['de', 'en-US', 'fr', 'ro', 'ru']),
/**
* @default null
* Value of the picker
*/
value: PropTypes.any,
value: PropTypes.instanceOf(Date),
/**
* Callback fired when the value (the selected date) changes @DateIOType.
*/
onChange: PropTypes.func,
/**
* Dedicated button for clearing the value
* @default 'date'
* Choose the type of picker you want displayed by the component
*/
isClearable: PropTypes.bool,
showPicker: PropTypes.oneOf(['date', 'dateTime', 'time']),
/**
* @default false
* Control the popup or dialog open state.
* @default {}
* The props used for each component slot.
*/
open: PropTypes.bool,
slotProps: PropTypes.object,
/**
* Callback fired when the popup requests to be closed. Use in controlled mode (see open).
* @default {}
* Override component slots.
*/
onClose: PropTypes.func,
slots: PropTypes.object,
/**
* If true, the picker and text field are disabled.
* @default 'ro'
* A small sample of ISO format names that will be used to display the date.
*/
disabled: PropTypes.bool,
localeFormat: PropTypes.oneOf(['de', 'en-US', 'fr', 'ro']),
/**
* If true, the label is displayed in an error state.
*/
Expand Down
Loading

0 comments on commit 4778290

Please sign in to comment.