Created
June 18, 2020 13:20
-
-
Save elcodabra/3984fb03e1b8adb8d1532d6469475cd8 to your computer and use it in GitHub Desktop.
Custom MUI KeyboardDatePicker with suggestions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef, useState } from 'react' | |
import moment from 'moment' | |
import InputAdornment from '@material-ui/core/InputAdornment' | |
import FormControl from '@material-ui/core/FormControl' | |
import TextField from '@material-ui/core/TextField' | |
import MenuItem from '@material-ui/core/MenuItem' | |
import Select from '@material-ui/core/Select' | |
import Paper from '@material-ui/core/Paper' | |
import ClickAwayListener from '@material-ui/core/ClickAwayListener' | |
import MenuList from '@material-ui/core/MenuList' | |
import ClearIcon from '@material-ui/icons/Clear' | |
import { KeyboardDateTimePicker, KeyboardDatePicker } from '@material-ui/pickers' | |
import { IconButton } from '@material-ui/core' | |
const TIME_TYPES = { | |
HOUR: 'hours', | |
DAY: 'days', | |
} | |
// TODO: getCorrectWord | |
const TYPES_LIST = [ | |
{ id: TIME_TYPES.HOUR, name: 'часа' }, | |
{ id: TIME_TYPES.DAY, name: 'дня' }, | |
] | |
const SUGGESTIONS_LIST = [ | |
{ id: 0, type: TIME_TYPES.HOUR, value: 0, name: 'по текущее время' }, | |
{ id: 1, type: TIME_TYPES.HOUR, value: 1, name: 'за 1 час' }, | |
{ id: 2, type: TIME_TYPES.HOUR, value: 2, name: 'за 2 часа' }, | |
{ id: 3, type: TIME_TYPES.DAY, value: 1, name: 'за 1 день' }, | |
{ id: 4, type: TIME_TYPES.DAY, value: 1, name: 'за 2 дня' }, | |
] | |
const MyTextField = React.memo((props) => { | |
console.log('PeriodTypeKeyboardNew=', props) | |
return ( | |
<TextField | |
style={{ minWidth: 200 }} | |
{...props} | |
/> | |
) | |
}, (prevProps, nextProps) => { | |
return prevProps.value === nextProps.value | |
&& prevProps.disabled === nextProps.disabled | |
}) | |
function makeMaskFromFormat(format, numberMaskChar) { | |
return format.replace(/[a-z]/gi, numberMaskChar); | |
} | |
function maskedDateFormatter(format, numberMaskChar, refuse) { | |
return function (value) { | |
var mask = makeMaskFromFormat(format, numberMaskChar) | |
var result = ''; | |
var parsed = value.replace(refuse, ''); | |
if (parsed === '') { | |
return parsed; | |
} | |
var i = 0; | |
var n = 0; | |
while (i < mask.length) { | |
var maskChar = mask[i]; | |
if (maskChar === numberMaskChar && n < parsed.length) { | |
var parsedChar = parsed[n]; | |
result += parsedChar; | |
n += 1; | |
} else { | |
result += maskChar; | |
} | |
i += 1; | |
} | |
return result; | |
}; | |
} | |
const PeriodTypeKeyboard = ({ | |
mode, | |
currentDate, | |
...calendarProps | |
}) => { | |
const [input, setInput] = useState(null) | |
const [date, setDate] = useState(null) | |
const [open, setOpen] = useState(false) | |
const [readOnly, setReadOnly] = useState(false) | |
const [type, setType] = useState(TIME_TYPES.HOUR) | |
const anchorRef = useRef(null) | |
const handleChange = (date, value) => { | |
setInput(value) | |
setDate(value ? moment().subtract(value, type) : null) | |
handleClose() | |
} | |
const handleSelectDate = (date) => { | |
setDate(date) | |
setType(null) | |
setInput(null) | |
setReadOnly(false) | |
handleClose() | |
} | |
const handleChangeType = (event) => { | |
setType(event.target.value) | |
} | |
const handleOpen = () => { | |
setOpen(true) | |
} | |
const handleClose = () => { | |
setOpen(false) | |
if (anchorRef.current && anchorRef.current.focus) { | |
anchorRef.current.focus() | |
} | |
} | |
const handleListKeyDown = (event) => { | |
if (event.key === 'Tab') { | |
event.preventDefault() | |
handleClose() | |
} | |
} | |
const handleSelectSuggestion = ({ type, value, name }) => { | |
setInput(name) | |
setDate(moment().subtract(value, type)) | |
setType(null) | |
setReadOnly(true) | |
handleClose() | |
} | |
const handleClearSuggestion = (event) => { | |
event.stopPropagation() | |
setType(TIME_TYPES.HOUR) | |
setReadOnly(false) | |
setInput(null) | |
setDate(null) | |
handleClose() | |
} | |
// TODO: memo | |
const labelFn = (date, invalidLabel) => { | |
// console.log('invalidLabel=', invalidLabel) | |
return input || (date ? date.format(calendarProps.format) : '') | |
} | |
const formatter = React.useMemo(() => { | |
if (type) return str => str | |
return maskedDateFormatter(calendarProps.format, '_', /[^\d]+/gi); | |
}, [calendarProps.format, type]) | |
useEffect(() => { | |
console.log("useEffect log:", anchorRef.current); | |
}, [anchorRef]) | |
const KeyboardPicker = React.useMemo(() => { | |
return mode === 'DATETIME' ? KeyboardDateTimePicker : KeyboardDatePicker | |
}, [mode]) | |
return ( | |
<div style={{ position: 'relative' }}> | |
<KeyboardPicker | |
// variant="inline" // TODO: ломает anchorRef | |
inputProps={{ | |
style: { | |
textAlign: 'center', | |
// fontSize: 'larger', | |
paddingLeft: 0, | |
paddingRight: 0, | |
} | |
}} | |
{...calendarProps} | |
inputRef={anchorRef} | |
value={date} | |
onAccept={handleSelectDate} | |
onChange={handleChange} | |
labelFunc={labelFn} | |
rifmFormatter={formatter} | |
TextFieldComponent={MyTextField} | |
DialogProps={{ | |
onExited: handleClose, | |
}} | |
KeyboardButtonProps={{ | |
size: 'small', | |
}} | |
InputAdornmentProps={{ | |
position: 'start', | |
onClick: (event) => { event.stopPropagation() } | |
}} | |
InputProps={{ | |
readOnly, | |
onClick: handleOpen, | |
endAdornment: ( | |
<InputAdornment position="end"> | |
{type ? ( | |
<FormControl> | |
<Select | |
displayEmpty | |
disableUnderline | |
value={type} | |
onClick={(e) => { | |
e.stopPropagation() | |
setOpen(false) | |
}} | |
onChange={handleChangeType} | |
> | |
{TYPES_LIST.map(({ id, name }) => ( | |
<MenuItem key={id} value={id}>{name}</MenuItem> | |
))} | |
</Select> | |
</FormControl> | |
) : ( | |
<IconButton size="small" onClick={handleClearSuggestion}> | |
<ClearIcon /> | |
</IconButton> | |
)} | |
</InputAdornment> | |
), | |
}} | |
/> | |
{open && ( | |
<Paper style={{ position: 'absolute', zIndex: 1, width: '100%' }}> | |
<ClickAwayListener onClickAway={handleClose}> | |
<div> | |
<MenuList onKeyDown={handleListKeyDown}> | |
{SUGGESTIONS_LIST.map(item => { | |
if (item.id === 0 && !currentDate) { | |
return null | |
} | |
return ( | |
<MenuItem | |
key={item.id} | |
value={item.id} | |
onClick={() => handleSelectSuggestion(item)} | |
> | |
{item.name} | |
</MenuItem> | |
) | |
})} | |
</MenuList> | |
</div> | |
</ClickAwayListener> | |
</Paper> | |
)} | |
</div> | |
) | |
} | |
export default PeriodTypeKeyboard |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef, useState } from 'react' | |
import moment from 'moment' | |
import InputAdornment from '@material-ui/core/InputAdornment' | |
import FormControl from '@material-ui/core/FormControl' | |
import TextField from '@material-ui/core/TextField' | |
import MenuItem from '@material-ui/core/MenuItem' | |
import Select from '@material-ui/core/Select' | |
import EventIcon from '@material-ui/icons/Event' | |
import Paper from '@material-ui/core/Paper' | |
import ClickAwayListener from '@material-ui/core/ClickAwayListener' | |
import MenuList from '@material-ui/core/MenuList' | |
import ClearIcon from '@material-ui/icons/Clear' | |
import { KeyboardDateTimePicker, DatePicker, DateTimePicker } from '@material-ui/pickers' | |
const TIME_TYPES = { | |
HOUR: 'hours', | |
DAY: 'days', | |
} | |
// TODO: getCorrectWord | |
const TYPES_LIST = [ | |
{ id: TIME_TYPES.HOUR, name: 'часа' }, | |
{ id: TIME_TYPES.DAY, name: 'дня' }, | |
] | |
const SUGGESTIONS_LIST = [ | |
{ id: 0, type: TIME_TYPES.HOUR, value: 0, name: 'по текущее время' }, | |
{ id: 1, type: TIME_TYPES.HOUR, value: 1, name: 'за 1 час' }, | |
{ id: 2, type: TIME_TYPES.HOUR, value: 2, name: 'за 2 часа' }, | |
{ id: 3, type: TIME_TYPES.DAY, value: 1, name: 'за 1 день' }, | |
{ id: 4, type: TIME_TYPES.DAY, value: 1, name: 'за 2 дня' }, | |
] | |
const MyTextField = React.memo((props) => { | |
console.log('PeriodTypeKeyboard=', props) | |
return ( | |
<TextField | |
style={{ minWidth: 200 }} | |
{...props} | |
/> | |
)}, (prevProps, nextProps) => { | |
return prevProps.value === nextProps.value | |
&& prevProps.disabled === nextProps.disabled | |
}) | |
function makeMaskFromFormat(format, numberMaskChar) { | |
return format.replace(/[a-z]/gi, numberMaskChar); | |
} | |
function maskedDateFormatter(format, numberMaskChar, refuse) { | |
return function (value) { | |
var mask = makeMaskFromFormat(format, numberMaskChar) | |
var result = ''; | |
var parsed = value.replace(refuse, ''); | |
if (parsed === '') { | |
return parsed; | |
} | |
var i = 0; | |
var n = 0; | |
while (i < mask.length) { | |
var maskChar = mask[i]; | |
if (maskChar === numberMaskChar && n < parsed.length) { | |
var parsedChar = parsed[n]; | |
result += parsedChar; | |
n += 1; | |
} else { | |
result += maskChar; | |
} | |
i += 1; | |
} | |
return result; | |
}; | |
} | |
const PeriodTypeKeyboardWithCustomDialog = ({ | |
mode, | |
currentDate, | |
...calendarProps | |
}) => { | |
const [readOnly, setReadOnly] = useState(false) | |
const [input, setInput] = useState(null) | |
// TODO: date from props | |
const [date, setDate] = useState(null) | |
const [open, setOpen] = useState(null) | |
const [type, setType] = useState(TIME_TYPES.HOUR) | |
const anchorRef = useRef(null) | |
const handleChange = (date, value) => { | |
setInput(value) | |
setDate(value ? moment().subtract(value, type) : null) | |
handleClose() | |
} | |
const handleSelectDate = (date) => { | |
setDate(date) | |
setType(null) | |
setInput(null) | |
setReadOnly(false) | |
handleClose() | |
} | |
const handleChangeType = (event) => { | |
setType(event.target.value) | |
} | |
const handleOpen = (open) => { | |
setOpen(open) | |
} | |
const handleClose = () => { | |
setOpen(null) | |
if (anchorRef.current && anchorRef.current.focus) { | |
anchorRef.current.focus() | |
} | |
} | |
const handleListKeyDown = (event) => { | |
if (event.key === 'Tab') { | |
event.preventDefault() | |
handleOpen(null) | |
} | |
} | |
const handleSelectExactlyDate = (event) => { | |
event.stopPropagation() | |
handleOpen('picker') | |
} | |
const handleSelectSuggestion = ({ type, value, name }) => { | |
setInput(name) | |
setDate(moment().subtract(value, type)) | |
setType(null) | |
setReadOnly(true) | |
handleClose() | |
} | |
const handleClearSuggestion = (event) => { | |
event.stopPropagation() | |
setType(TIME_TYPES.HOUR) | |
setReadOnly(false) | |
setInput(null) | |
setDate(null) | |
handleClose() | |
} | |
// TODO: memo | |
const labelFn = (date, invalidLabel) => { | |
// console.log('invalidLabel=', invalidLabel) | |
return input || (date ? date.format(calendarProps.format) : '') | |
} | |
const formatter = React.useMemo(() => { | |
if (type) return str => str | |
return maskedDateFormatter(calendarProps.format, '_', /[^\d]+/gi); | |
}, [calendarProps.format, type]) | |
useEffect(() => { | |
console.log("useEffect log:", anchorRef.current); | |
}, [anchorRef]) | |
const Picker = mode === 'DATETIME' ? DateTimePicker : DatePicker | |
return ( | |
<div style={{ position: 'relative' }}> | |
<KeyboardDateTimePicker | |
inputProps={{ | |
style: { | |
textAlign: 'center', | |
// fontSize: 'larger', | |
paddingLeft: 15, | |
paddingRight: 15, | |
} | |
}} | |
{...calendarProps} | |
inputRef={anchorRef} | |
value={date} | |
onChange={handleChange} | |
labelFunc={labelFn} | |
rifmFormatter={formatter} | |
TextFieldComponent={MyTextField} | |
InputAdornmentProps={{ | |
position: 'start', | |
component: () => ( | |
<EventIcon color="primary" onClick={handleSelectExactlyDate} /> | |
), | |
}} | |
InputProps={{ | |
readOnly, | |
onClick: () => handleOpen(open || 'default'), | |
endAdornment: type ? ( | |
<InputAdornment position="start"> | |
<FormControl> | |
<Select | |
displayEmpty | |
disableUnderline | |
value={type} | |
onClick={(e) => { | |
e.stopPropagation() | |
setOpen(false) | |
}} | |
onChange={handleChangeType} | |
> | |
{TYPES_LIST.map(({ id, name }) => ( | |
<MenuItem key={id} value={id}>{name}</MenuItem> | |
))} | |
</Select> | |
</FormControl> | |
</InputAdornment> | |
) : ( | |
<ClearIcon fontSize="small" onClick={handleClearSuggestion} /> | |
), | |
}} | |
/> | |
{open && ( | |
<Paper style={{ position: 'absolute', zIndex: 1, width: '100%' }}> | |
<ClickAwayListener onClickAway={handleClose}> | |
<div> | |
{open === 'picker' ? ( | |
<Picker | |
autoOk | |
openTo="date" | |
{...calendarProps} | |
variant="static" | |
value={input} | |
onChange={handleSelectDate} | |
/> | |
) : ( | |
<MenuList onKeyDown={handleListKeyDown}> | |
{SUGGESTIONS_LIST.map(item => { | |
if (item.id === 0 && !currentDate) { | |
return null | |
} | |
return ( | |
<MenuItem | |
key={item.id} | |
value={item.id} | |
onClick={() => handleSelectSuggestion(item)} | |
> | |
{item.name} | |
</MenuItem> | |
) | |
})} | |
</MenuList> | |
)} | |
</div> | |
</ClickAwayListener> | |
</Paper> | |
)} | |
</div> | |
) | |
} | |
export default PeriodTypeKeyboardWithCustomDialog |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useRef, useState } from 'react' | |
import InputAdornment from '@material-ui/core/InputAdornment' | |
import FormControl from '@material-ui/core/FormControl' | |
import TextField from '@material-ui/core/TextField' | |
import MenuItem from '@material-ui/core/MenuItem' | |
import Select from '@material-ui/core/Select' | |
import EventIcon from '@material-ui/icons/Event' | |
import Paper from '@material-ui/core/Paper' | |
import ClickAwayListener from '@material-ui/core/ClickAwayListener' | |
import MenuList from '@material-ui/core/MenuList' | |
import { DatePicker } from '@material-ui/pickers' | |
const TIME_TYPES = { | |
HOUR: 'h', | |
DAY: 'd', | |
} | |
const TYPES_LIST = [ | |
{ id: TIME_TYPES.HOUR, name: 'часа' }, | |
{ id: TIME_TYPES.DAY, name: 'дня' }, | |
] | |
const SUGGESTIONS_LIST = [ | |
{ id: 0, type: TIME_TYPES.HOUR, value: 0, name: 'по текущее время' }, | |
{ id: 1, type: TIME_TYPES.HOUR, value: 1, name: 'за 1 час' }, | |
{ id: 2, type: TIME_TYPES.HOUR, value: 2, name: 'за 2 часа' }, | |
{ id: 3, type: TIME_TYPES.DAY, value: 1, name: 'за 1 день' }, | |
{ id: 4, type: TIME_TYPES.DAY, value: 1, name: 'за 2 дня' }, | |
] | |
const PeriodTypeTextField = ({ | |
currentDate, | |
textProps, | |
...calendarProps | |
}) => { | |
const [input, setInput] = useState(currentDate ? SUGGESTIONS_LIST[0].name : '') | |
const [readOnly, setReadOnly] = useState(currentDate) | |
const [type, setType] = useState(currentDate ? null : TIME_TYPES.HOUR) | |
const [open, setOpen] = useState(null) | |
const anchorRef = useRef(null) | |
/* | |
const handleToggle = () => { | |
setOpen((prevOpen) => !prevOpen) | |
} | |
*/ | |
const handleOpen = (type) => { | |
setOpen(type) | |
} | |
const handleClose = (event) => { | |
if (anchorRef.current && anchorRef.current.contains(event.target)) { | |
return; | |
} | |
setOpen(null) | |
} | |
function handleListKeyDown(event) { | |
if (event.key === 'Tab') { | |
event.preventDefault() | |
setOpen(null) | |
} | |
} | |
// return focus to the input when we transitioned from !open -> open | |
const prevOpen = React.useRef(open) | |
React.useEffect(() => { | |
if (prevOpen.current && !open) { | |
anchorRef.current && anchorRef.current.focus && anchorRef.current.focus() | |
} | |
prevOpen.current = open | |
}, [open]) | |
const handleInputChange = (event) => { | |
setOpen(null) | |
setInput(event.target.value) | |
console.log(`${event.target.value} ${type}`) | |
} | |
const handleChangeType = (event) => { | |
setType(event.target.value) | |
console.log(`${input} ${event.target.value}`) | |
} | |
const handleSelectSuggestion = (event, item) => { | |
setType(null) | |
setReadOnly(true) | |
setInput(item.name) | |
// handleChange(item.type, item.value) | |
handleClose(event) | |
} | |
const handleSelectExactlyDate = (event) => { | |
event.stopPropagation() | |
setOpen('picker') | |
} | |
const handleChangeDate = (date) => { | |
setInput(date.format()) | |
setType(null) | |
setOpen(null) | |
} | |
const handleClearSuggestion = (event) => { | |
event.stopPropagation() | |
setType(TIME_TYPES.HOUR) | |
setReadOnly(false) | |
setInput('') | |
if (anchorRef.current && anchorRef.current.focus) { | |
anchorRef.current.focus() | |
} | |
// handleChange(item.type, item.value) | |
} | |
return ( | |
<div style={{ position: 'relative' }}> | |
<TextField | |
style={{ minWidth: 250 }} | |
fullWidth | |
// autoFocus | |
margin="dense" | |
// variant="outlined" | |
// type="number" | |
// error="numbers" | |
inputProps={{ | |
style: { | |
textAlign: 'center', | |
fontSize: 'larger', | |
paddingLeft: 15, | |
paddingRight: 15, | |
} | |
}} | |
InputLabelProps={{ | |
shrink: true, | |
}} | |
{...textProps} | |
inputRef={anchorRef} | |
aria-controls={open ? 'menu-list-grow' : undefined} | |
aria-haspopup="true" | |
value={input} | |
onChange={handleInputChange} | |
InputProps={{ | |
readOnly, | |
onClick: () => handleOpen(open || 'default'), | |
startAdornment: ( | |
<> | |
<EventIcon color="primary" onClick={handleSelectExactlyDate} /> | |
{type && (<span style={{ paddingLeft: 15 }}>за</span>)} | |
</> | |
), | |
endAdornment: type ? ( | |
<InputAdornment position="start"> | |
<FormControl> | |
<Select | |
displayEmpty | |
disableUnderline | |
value={type} | |
onClick={(e) => { | |
e.stopPropagation() | |
setOpen(false) | |
}} | |
onChange={handleChangeType} | |
> | |
{TYPES_LIST.map(({ id, name }) => ( | |
<MenuItem key={id} value={id}>{name}</MenuItem> | |
))} | |
</Select> | |
</FormControl> | |
</InputAdornment> | |
) : ( | |
<span onClick={handleClearSuggestion}>[x]</span> | |
), | |
}} | |
/> | |
{open && ( | |
<Paper style={{ position: 'absolute', zIndex: 1, width: '100%' }}> | |
<ClickAwayListener onClickAway={handleClose}> | |
<div> | |
{open === 'picker' ? ( | |
<DatePicker | |
autoOk | |
openTo="date" | |
{...calendarProps} | |
variant="static" | |
onChange={handleChangeDate} | |
/> | |
) : ( | |
<MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}> | |
<MenuItem onClick={handleSelectExactlyDate}> | |
<EventIcon color="primary" style={{ marginRight: 5 }} /> | |
указать точную дату | |
</MenuItem> | |
{SUGGESTIONS_LIST.map(item => { | |
if (item.id === 0 && !currentDate) { | |
return null | |
} | |
return ( | |
<MenuItem | |
key={item.id} | |
value={item.id} | |
onClick={(e) => handleSelectSuggestion(e, item)} | |
> | |
{item.name} | |
</MenuItem> | |
) | |
})} | |
</MenuList> | |
)} | |
</div> | |
</ClickAwayListener> | |
</Paper> | |
)} | |
</div> | |
) | |
} | |
export default PeriodTypeTextField |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment