* [#710] added custom mobile timePicker * [#710] custom mobile datepicker
This commit is contained in:
@@ -1,14 +1,8 @@
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker'
|
||||
import { useMediaQuery } from '@linagora/twake-mui'
|
||||
import { PickerValue } from '@mui/x-date-pickers/internals'
|
||||
import { Dayjs } from 'dayjs'
|
||||
import React from 'react'
|
||||
import { LONG_DATE_FORMAT } from '../../utils/dateTimeFormatters'
|
||||
import { ReadOnlyDateField } from '../ReadOnlyPickerField'
|
||||
import {
|
||||
dateCalendarLayoutSx,
|
||||
getDateFieldSlotProps,
|
||||
getDateSlotProps
|
||||
} from './dateTimePickerSlotProps'
|
||||
import { DesktopDatePickerField } from './DesktopDatePickerField'
|
||||
import { TouchDatePickerField } from './TouchDatePickerField'
|
||||
|
||||
export interface DatePickerFieldProps {
|
||||
value: Dayjs | null
|
||||
@@ -18,22 +12,11 @@ export interface DatePickerFieldProps {
|
||||
hasError?: boolean
|
||||
}
|
||||
|
||||
export const DatePickerField: React.FC<DatePickerFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
testId,
|
||||
label,
|
||||
hasError = false
|
||||
}) => (
|
||||
<DatePicker
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
slots={{ field: ReadOnlyDateField }}
|
||||
slotProps={{
|
||||
...getDateSlotProps(testId, hasError, label),
|
||||
field: getDateFieldSlotProps(testId, hasError, label),
|
||||
layout: { sx: dateCalendarLayoutSx }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
export const DatePickerField: React.FC<DatePickerFieldProps> = props => {
|
||||
const isTouch = useMediaQuery('(pointer: coarse)')
|
||||
return isTouch ? (
|
||||
<TouchDatePickerField {...props} />
|
||||
) : (
|
||||
<DesktopDatePickerField {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker'
|
||||
import React from 'react'
|
||||
import { LONG_DATE_FORMAT } from '../../utils/dateTimeFormatters'
|
||||
import { ReadOnlyDateField } from '../ReadOnlyPickerField'
|
||||
import {
|
||||
dateCalendarLayoutSx,
|
||||
getDateFieldSlotProps,
|
||||
getDateSlotProps
|
||||
} from './dateTimePickerSlotProps'
|
||||
import { DatePickerFieldProps } from './DatePickerField'
|
||||
|
||||
export const DesktopDatePickerField: React.FC<DatePickerFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
testId,
|
||||
label,
|
||||
hasError = false
|
||||
}) => (
|
||||
<DatePicker
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
slots={{ field: ReadOnlyDateField }}
|
||||
slotProps={{
|
||||
...getDateSlotProps(testId, hasError, label),
|
||||
field: getDateFieldSlotProps(testId, hasError, label),
|
||||
layout: { sx: dateCalendarLayoutSx }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
import { TimePicker } from '@mui/x-date-pickers/TimePicker'
|
||||
import React from 'react'
|
||||
import { EditableTimeField } from '../EditableTimeField'
|
||||
import {
|
||||
getTimeFieldSlotProps,
|
||||
timePickerPopperSx
|
||||
} from './dateTimePickerSlotProps'
|
||||
import { TimePickerFieldProps } from './TimePickerField'
|
||||
|
||||
export const DesktopTimePickerField: React.FC<TimePickerFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
testId,
|
||||
label,
|
||||
hasError = false,
|
||||
disabled = false
|
||||
}) => (
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{
|
||||
field: EditableTimeField,
|
||||
actionBar: () => null
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { display: 'none' } },
|
||||
popper: { sx: timePickerPopperSx },
|
||||
field: getTimeFieldSlotProps(testId, hasError, label)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useMediaQuery } from '@linagora/twake-mui'
|
||||
import { PickerValue } from '@mui/x-date-pickers/internals'
|
||||
import { TimePicker } from '@mui/x-date-pickers/TimePicker'
|
||||
import { Dayjs } from 'dayjs'
|
||||
import React from 'react'
|
||||
import { EditableTimeField } from '../EditableTimeField'
|
||||
import {
|
||||
getTimeFieldSlotProps,
|
||||
timePickerPopperSx
|
||||
} from './dateTimePickerSlotProps'
|
||||
import { DesktopTimePickerField } from './DesktopTimePickerField'
|
||||
import { TouchTimePickerField } from './TouchTimePickerField'
|
||||
|
||||
export interface TimePickerFieldProps {
|
||||
value: Dayjs | null
|
||||
@@ -17,29 +14,11 @@ export interface TimePickerFieldProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const TimePickerField: React.FC<TimePickerFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
testId,
|
||||
label,
|
||||
hasError = false,
|
||||
disabled = false
|
||||
}) => (
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{
|
||||
field: EditableTimeField,
|
||||
actionBar: () => null
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { display: 'none' } },
|
||||
popper: { sx: timePickerPopperSx },
|
||||
field: getTimeFieldSlotProps(testId, hasError, label)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
export const TimePickerField: React.FC<TimePickerFieldProps> = props => {
|
||||
const isTouch = useMediaQuery('(pointer: coarse)')
|
||||
return isTouch ? (
|
||||
<TouchTimePickerField {...props} />
|
||||
) : (
|
||||
<DesktopTimePickerField {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import CalendarTodayIcon from '@mui/icons-material/CalendarToday'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
IconButton,
|
||||
Typography
|
||||
} from '@linagora/twake-mui'
|
||||
import { DateField, DatePicker } from '@mui/x-date-pickers'
|
||||
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'
|
||||
import { PickerValue } from '@mui/x-date-pickers/internals'
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import React, { useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { ReadOnlyDateField } from '../ReadOnlyPickerField'
|
||||
import { DatePickerFieldProps } from './DatePickerField'
|
||||
import {
|
||||
dateCalendarLayoutSx,
|
||||
getDateFieldSlotProps,
|
||||
getDateSlotProps
|
||||
} from './dateTimePickerSlotProps'
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
const DISPLAY_FORMAT = 'D MMMM, YYYY'
|
||||
|
||||
interface DatePickerDialogProps {
|
||||
value: PickerValue
|
||||
onChange: (value: PickerValue) => void
|
||||
onAccept: (value: PickerValue) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const DatePickerDialogContent: React.FC<DatePickerDialogProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onAccept,
|
||||
onCancel
|
||||
}) => {
|
||||
const { t } = useI18n()
|
||||
|
||||
const [viewMode, setViewMode] = useState<'calendar' | 'text'>('calendar')
|
||||
|
||||
const [internalValue, setInternalValue] = useState<PickerValue>(value)
|
||||
|
||||
const handleInternalChange = (newValue: PickerValue): void => {
|
||||
setInternalValue(newValue)
|
||||
onChange(newValue)
|
||||
}
|
||||
|
||||
const handleToggleView = (): void => {
|
||||
setViewMode(viewMode === 'calendar' ? 'text' : 'calendar')
|
||||
}
|
||||
|
||||
const displayDate = internalValue?.isValid()
|
||||
? internalValue.format(DISPLAY_FORMAT)
|
||||
: t('dateTimeFields.pickADate')
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, width: 280 }}>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mb: 1,
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight={500}>
|
||||
{displayDate}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleToggleView}
|
||||
aria-label={
|
||||
viewMode === 'calendar'
|
||||
? t('dateTimeFields.switchToTextInput')
|
||||
: t('dateTimeFields.switchToCalendar')
|
||||
}
|
||||
>
|
||||
{viewMode === 'calendar' ? (
|
||||
<EditIcon fontSize="small" />
|
||||
) : (
|
||||
<CalendarTodayIcon fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Body */}
|
||||
{viewMode === 'calendar' ? (
|
||||
<DateCalendar
|
||||
value={internalValue}
|
||||
onChange={handleInternalChange}
|
||||
sx={{ m: 0, width: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ mt: 1, mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('dateTimeFields.date')}
|
||||
</Typography>
|
||||
<DateField
|
||||
fullWidth
|
||||
inputMode="numeric"
|
||||
size="small"
|
||||
value={internalValue}
|
||||
onChange={handleInternalChange}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
textField: {
|
||||
inputProps: { inputMode: 'numeric', pattern: '[0-9]*' }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Button onClick={onCancel} variant="text">
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={() => onAccept(internalValue)} variant="contained">
|
||||
{t('common.ok')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export const TouchDatePickerField: React.FC<DatePickerFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
testId,
|
||||
label = 'Date',
|
||||
hasError = false
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [pendingValue, setPendingValue] = useState<PickerValue>(value)
|
||||
|
||||
const handleOpen = (): void => {
|
||||
setPendingValue(value)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleAccept = (newValue: PickerValue): void => {
|
||||
onChange(newValue)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleCancel = (): void => {
|
||||
setPendingValue(value)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DatePicker
|
||||
label={label}
|
||||
value={value}
|
||||
onOpen={handleOpen}
|
||||
onAccept={onChange}
|
||||
open={false}
|
||||
slots={{ field: ReadOnlyDateField }}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { display: 'none' } },
|
||||
...getDateSlotProps(testId, hasError, label),
|
||||
field: getDateFieldSlotProps(testId, hasError, label),
|
||||
layout: { sx: dateCalendarLayoutSx }
|
||||
}}
|
||||
/>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleCancel}
|
||||
slotProps={{ paper: { sx: { borderRadius: 2, overflow: 'hidden' } } }}
|
||||
>
|
||||
<DatePickerDialogContent
|
||||
value={pendingValue}
|
||||
onChange={setPendingValue}
|
||||
onAccept={handleAccept}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
Divider,
|
||||
IconButton,
|
||||
SxProps,
|
||||
Theme
|
||||
} from '@linagora/twake-mui'
|
||||
import AccessTime from '@mui/icons-material/AccessTime'
|
||||
import KeyboardAltOutlined from '@mui/icons-material/KeyboardAltOutlined'
|
||||
import {
|
||||
MobileTimePicker,
|
||||
renderTimeViewClock,
|
||||
TimeField
|
||||
} from '@mui/x-date-pickers'
|
||||
import { PickerValue } from '@mui/x-date-pickers/internals'
|
||||
import { StaticTimePicker } from '@mui/x-date-pickers/StaticTimePicker'
|
||||
import { useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { EditableTimeField } from '../EditableTimeField'
|
||||
import { getTimeFieldSlotProps } from './dateTimePickerSlotProps'
|
||||
import { TimePickerFieldProps } from './TimePickerField'
|
||||
|
||||
const TIME_DISPLAY_SX = {
|
||||
fontSize: '48px',
|
||||
fontWeight: 400
|
||||
} as const
|
||||
|
||||
const TIME_INPUT_SX: SxProps<Theme> = {
|
||||
width: '100%',
|
||||
'& .MuiPickersInputBase-sectionAfter': { mx: '16px' },
|
||||
'& .MuiPickersSectionList-root': { justifyContent: 'end' },
|
||||
'& .MuiPickersInputBase-root': {
|
||||
...TIME_DISPLAY_SX,
|
||||
color: 'text.secondary',
|
||||
backgroundColor: 'transparent !important',
|
||||
'&:before': { borderBottomColor: 'transparent !important' },
|
||||
'&:after': { borderBottomColor: 'transparent !important' }
|
||||
},
|
||||
'& .MuiPickersInputBase-input': {
|
||||
textAlign: 'center'
|
||||
},
|
||||
'& .MuiPickersInputBase-root.Mui-focused .MuiPickersInputBase-input': {
|
||||
backgroundColor: 'transparent !important',
|
||||
color: 'primary.main'
|
||||
}
|
||||
}
|
||||
|
||||
const STATIC_PICKER_SX: SxProps<Theme> = {
|
||||
'& .MuiPickersToolbar-content': { justifyContent: 'center' },
|
||||
'& .MuiTimePickerToolbar-hourMinuteLabel': {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '16px'
|
||||
},
|
||||
'& .MuiTimePickerToolbar-hourMinuteLabel .MuiPickersToolbarText-root[data-selected]':
|
||||
{ color: 'primary.main' },
|
||||
'& .MuiPickersToolbar-root .MuiTypography-root': TIME_DISPLAY_SX,
|
||||
'& .MuiPickersToolbar-title': { display: 'none' },
|
||||
'& .MuiPickersArrowSwitcher-root': { display: 'none' }
|
||||
}
|
||||
|
||||
type DialogView = 'clock' | 'keyboard'
|
||||
|
||||
export const TouchTimePickerField: React.FC<TimePickerFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
testId,
|
||||
label,
|
||||
hasError = false,
|
||||
disabled = false
|
||||
}) => {
|
||||
const { t } = useI18n()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [internalValue, setInternalValue] = useState<PickerValue>(value)
|
||||
const [view, setView] = useState<DialogView>('clock')
|
||||
|
||||
const handleOpen = (): void => {
|
||||
setInternalValue(value)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleAccept = (): void => {
|
||||
onChange(internalValue)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleCancel = (): void => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const toggleView = (): void =>
|
||||
setView(view === 'clock' ? 'keyboard' : 'clock')
|
||||
|
||||
return (
|
||||
<>
|
||||
<MobileTimePicker
|
||||
ampm={false}
|
||||
value={value}
|
||||
onAccept={onChange}
|
||||
disabled={disabled}
|
||||
open={false}
|
||||
onOpen={handleOpen}
|
||||
viewRenderers={{
|
||||
hours: renderTimeViewClock,
|
||||
minutes: renderTimeViewClock
|
||||
}}
|
||||
slots={{ field: EditableTimeField }}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { display: 'none' } },
|
||||
field: {
|
||||
...getTimeFieldSlotProps(testId, hasError, label),
|
||||
onFocus: e => e.target.blur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog open={open} onClose={handleCancel}>
|
||||
{view === 'clock' && (
|
||||
<StaticTimePicker
|
||||
ampm={false}
|
||||
value={internalValue}
|
||||
onChange={setInternalValue}
|
||||
viewRenderers={{
|
||||
hours: renderTimeViewClock,
|
||||
minutes: renderTimeViewClock
|
||||
}}
|
||||
slots={{ actionBar: () => null }}
|
||||
sx={STATIC_PICKER_SX}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view === 'keyboard' && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '16px'
|
||||
}}
|
||||
>
|
||||
<TimeField
|
||||
ampm={false}
|
||||
value={internalValue}
|
||||
onChange={setInternalValue}
|
||||
autoFocus
|
||||
sx={TIME_INPUT_SX}
|
||||
slotProps={{
|
||||
textField: {
|
||||
variant: 'filled',
|
||||
InputLabelProps: { shrink: false },
|
||||
inputProps: { inputMode: 'numeric', pattern: '[0-9]*' }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<DialogActions
|
||||
sx={{ display: 'flex', justifyContent: 'space-between' }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={toggleView}
|
||||
aria-label={
|
||||
view === 'clock'
|
||||
? t('dateTimeFields.switchToTextInput')
|
||||
: t('dateTimeFields.switchToClockView')
|
||||
}
|
||||
>
|
||||
{view === 'clock' ? <KeyboardAltOutlined /> : <AccessTime />}
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Button onClick={handleCancel} variant="text">
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleAccept} variant="contained">
|
||||
{t('common.ok')}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+5
-1
@@ -347,7 +347,11 @@
|
||||
"startDate": "Start Date",
|
||||
"startTime": "Start Time",
|
||||
"endDate": "End Date",
|
||||
"endTime": "End Time"
|
||||
"endTime": "End Time",
|
||||
"pickADate": "Pick a date",
|
||||
"switchToTextInput": "Switch to text input",
|
||||
"switchToCalendar": "Switch to calendar view",
|
||||
"switchToClockView": "Switch to clock view"
|
||||
},
|
||||
"websocket": {
|
||||
"browserOnline": "You’re back online. Reconnecting live updates…",
|
||||
|
||||
+5
-1
@@ -348,7 +348,11 @@
|
||||
"startDate": "Date du début",
|
||||
"startTime": "Heure du début",
|
||||
"endDate": "Date de la fin",
|
||||
"endTime": "Heure de la fin"
|
||||
"endTime": "Heure de la fin",
|
||||
"pickADate": "Choisissez une date",
|
||||
"switchToTextInput": "Passer à la saisie de texte",
|
||||
"switchToCalendar": "Passer à la vue calendrier",
|
||||
"switchToClockView": "Passer à la vue horloge"
|
||||
},
|
||||
"websocket": {
|
||||
"browserOnline": "Vous êtes de nouveau en ligne. Reconnexion des mises à jour en temps réel…",
|
||||
|
||||
+5
-1
@@ -348,7 +348,11 @@
|
||||
"startDate": "Дата начала",
|
||||
"startTime": "Время начала",
|
||||
"endDate": "Дата окончания",
|
||||
"endTime": "Время окончания"
|
||||
"endTime": "Время окончания",
|
||||
"pickADate": "Выберите дату",
|
||||
"switchToTextInput": "Переключиться на текстовый ввод",
|
||||
"switchToCalendar": "Переключиться на календарь",
|
||||
"switchToClockView": "Переключиться на вид часов"
|
||||
},
|
||||
"websocket": {
|
||||
"browserOnline": "Вы снова в сети. Подключаем обновления в реальном времени…",
|
||||
|
||||
+5
-1
@@ -346,7 +346,11 @@
|
||||
"startDate": "Ngày bắt đầu",
|
||||
"startTime": "Giờ bắt đầu",
|
||||
"endDate": "Ngày kết thúc",
|
||||
"endTime": "Giờ kết thúc"
|
||||
"endTime": "Giờ kết thúc",
|
||||
"pickADate": "Chọn ngày",
|
||||
"switchToTextInput": "Chuyển sang nhập văn bản",
|
||||
"switchToCalendar": "Chuyển sang chế độ lịch",
|
||||
"switchToClockView": "Chuyển sang chế độ đồng hồ"
|
||||
},
|
||||
"websocket": {
|
||||
"browserOnline": "Bạn đã trực tuyến trở lại. Đang kết nối lại cập nhật thời gian thực…",
|
||||
|
||||
Reference in New Issue
Block a user