Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(calendar input): make input editable #1445

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion components/calendar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@
"@dhis2-ui/input": "9.2.0",
"@dhis2-ui/layer": "9.2.0",
"@dhis2-ui/popper": "9.2.0",
"@dhis2/multi-calendar-dates": "1.0.2",
"@dhis2/multi-calendar-dates": "v1.0.0-alpha.23",
"@dhis2/prop-types": "^3.1.2",
"@dhis2/ui-constants": "9.2.0",
"@dhis2/ui-icons": "9.2.0",
"@js-temporal/polyfill": "^0.4.2",
"classnames": "^2.3.1",
"prop-types": "^15.7.2"
},
Expand Down
206 changes: 206 additions & 0 deletions components/calendar/src/calendar-input/calendar-input-editable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { Button } from '@dhis2-ui/button'
import { InputField, InputFieldProps } from '@dhis2-ui/input'
import { useDatePicker, validateDateString } from '@dhis2/multi-calendar-dates'
import { IconCheckmark24 } from '@dhis2/ui-icons'
import PropTypes from 'prop-types'
import React, { useCallback, useRef, useState } from 'react'
import { CalendarProps } from '../calendar/calendar.js'
import { CalendarWidget } from './calendar-widget.js'
import { InputClearButton } from './input-clear-button.js'

function searchCalendarWeekDays(date, calendarWeekDays) {
for (let i = 0; i < calendarWeekDays.length; ++i) {
const days = calendarWeekDays[i]
const temporalDate = days.find(
({ calendarDate }) => calendarDate === date
)

if (temporalDate) {
return temporalDate.zdt
}
}

return null
}

export const CalendarInputEditable = ({
onDateSelect,
calendar,
cellSize,
clearable,
date,
dir,
locale,
numberingSystem,
weekDayFormat,
timeZone,
width,
onError,
...rest
} = {}) => {
const ref = useRef()
const [open, setOpen] = useState(false)
const [invalidMessage, _setInvalidMessage] = useState('')
const setInvalidMessage = useCallback(
(nextInvalidMessage) => {
_setInvalidMessage((prevInvalidMessage) => {
const isUpdate = prevInvalidMessage !== nextInvalidMessage

if (isUpdate) {
onError?.(nextInvalidMessage)
}

return nextInvalidMessage
})
},
[onError]
)

const [tempInputValue, _setTempInputValue] = useState(date)
const currentValidDate = validateDateString(tempInputValue, { calendar })
? tempInputValue
: date
const { calendarWeekDays } = useDatePicker({
date: currentValidDate,
options: { calendar },
})

const setTempInputValue = useCallback(
(nextTempValue) => {
_setTempInputValue(nextTempValue)
let nextInvalidMessage

try {
nextInvalidMessage = validateDateString(nextTempValue, {
calendar,
})
} catch (e) {
console.error(e)
}

if (!nextTempValue) {
onDateSelect({
calendarDate: null,
calendarDateString: '',
})
} else if (!nextInvalidMessage) {
setInvalidMessage('')
const calendarDate = searchCalendarWeekDays(
nextTempValue,
calendarWeekDays
)
onDateSelect({
calendarDate,
calendarDateString: nextTempValue,
})
} else {
setInvalidMessage(nextInvalidMessage)
}
},
[onDateSelect, calendarWeekDays, setInvalidMessage, calendar]
)

const onDateSelectWrapper = useCallback(
(selectedDate) => {
setOpen(false)
setTempInputValue(selectedDate?.calendarDateString || '')
},
[setTempInputValue]
)

const showInvalidMessage = !!tempInputValue && !!invalidMessage
const warning = rest.warning || (!rest.required && showInvalidMessage)
const error = rest.error || (rest.required && !!showInvalidMessage)

return (
<>
<div className="calendar-input-wrapper" ref={ref}>
<div className="input">
<InputField
label="Pick a date"
{...rest}
type="text"
value={tempInputValue}
onChange={({ value }) => setTempInputValue(value)}
warning={
warning || (!rest.required && showInvalidMessage)
}
error={error || (rest.required && !!showInvalidMessage)}
helpText={
rest.helpText || (tempInputValue && invalidMessage)
}
/>
</div>

{clearable && (
<InputClearButton
dense={rest.dense}
error={error}
loading={rest.loading}
insetBlockStartButton="27px"
insetInlineEndButton="51px"
insetInlineEndIcon="81px"
valid={rest.valid}
warning={warning}
onDateSelect={(selectedDate) => {
setOpen(false)
onDateSelect?.(selectedDate)
}}
/>
)}

<div className="open-calendar-widget">
<Button
icon={<IconCheckmark24 color="#000000" />}
onClick={() => setOpen(true)}
/>
</div>
</div>

{open && (
<CalendarWidget
onClose={() => setOpen(false)}
onDateSelect={onDateSelectWrapper}
calendar={calendar}
date={date}
dir={dir}
locale={locale}
numberingSystem={numberingSystem}
weekDayFormat={weekDayFormat}
timeZone={timeZone}
width={width}
cellSize={cellSize}
/>
)}

<style jsx>
{`
.calendar-input-wrapper {
position: relative;
display: flex;
gap: 8px;
}

.input {
flex-grow: 1;
}

.open-calendar-widget {
padding-top: 22px;
flex-shrink: 1;
}
`}
</style>
</>
)
}

CalendarInputEditable.defaultProps = {
dataTest: 'dhis2-uiwidgets-calendar-inputfield',
}

CalendarInputEditable.propTypes = {
...CalendarProps,
...InputFieldProps,
onError: PropTypes.func,
}
143 changes: 40 additions & 103 deletions components/calendar/src/calendar-input/calendar-input.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { Button } from '@dhis2-ui/button'
import { Card } from '@dhis2-ui/card'
import { InputField, InputFieldProps } from '@dhis2-ui/input'
import { Layer } from '@dhis2-ui/layer'
import { Popper } from '@dhis2-ui/popper'
import cx from 'classnames'
import React, { useRef, useState } from 'react'
import { Calendar, CalendarProps } from '../calendar/calendar.js'
import i18n from '../locales/index.js'

const offsetModifier = {
name: 'offset',
options: {
offset: [0, 2],
},
}
import { CalendarProps } from '../calendar/calendar.js'
import { CalendarWidget } from './calendar-widget.js'
import { InputClearButton } from './input-clear-button.js'

export const CalendarInput = ({
onDateSelect,
Expand All @@ -31,40 +20,7 @@ export const CalendarInput = ({
} = {}) => {
const ref = useRef()
const [open, setOpen] = useState(false)

const calendarProps = React.useMemo(() => {
const onDateSelectWrapper = (selectedDate) => {
setOpen(false)
onDateSelect?.(selectedDate)
}
return {
onDateSelect: onDateSelectWrapper,
calendar,
date,
dir,
locale,
numberingSystem,
weekDayFormat,
timeZone,
width,
cellSize,
}
}, [
calendar,
cellSize,
date,
dir,
locale,
numberingSystem,
onDateSelect,
timeZone,
weekDayFormat,
width,
])

const onFocus = () => {
setOpen(true)
}
const onFocus = () => setOpen(true)

return (
<>
Expand All @@ -76,76 +32,57 @@ export const CalendarInput = ({
onFocus={onFocus}
value={date}
/>

{clearable && (
<div
className={cx('calendar-clear-button', {
// ToDo: this is a workaround to show the clear button in the correct place when an icon is shown.
// Long-term, we should abstract and share the logic multi-select uses for the input-wrapper
// https://dhis2.atlassian.net/browse/DHIS2-14848
'with-icon':
rest.valid ||
rest.error ||
rest.warning ||
rest.loading,
'with-dense-wrapper': rest.dense,
})}
>
<Button
dataTest="calendar-clear-button"
secondary
small
onClick={() => calendarProps.onDateSelect(null)}
type="button"
>
{i18n.t('Clear')}
</Button>
</div>
<InputClearButton
dense={rest.dense}
error={rest.error}
loading={rest.loading}
insetBlockStartButton="27px"
insetInlineEndButton="6px"
insetInlineEndIcon="36px"
valid={rest.valid}
warning={rest.warning}
onDateSelect={(selectedDate) => {
setOpen(false)
onDateSelect?.(selectedDate)
}}
/>
)}
</div>

{open && (
<Layer
onBackdropClick={() => {
<CalendarWidget
onClose={() => setOpen(false)}
onDateSelect={(selectedDate) => {
setOpen(false)
onDateSelect?.(selectedDate)
}}
>
<Popper
reference={ref}
placement="bottom-start"
modifiers={[offsetModifier]}
>
<Card>
<Calendar {...calendarProps} date={date} />
</Card>
</Popper>
</Layer>
calendar={calendar}
date={date}
dir={dir}
locale={locale}
numberingSystem={numberingSystem}
weekDayFormat={weekDayFormat}
timeZone={timeZone}
width={width}
cellSize={cellSize}
/>
)}

<style jsx>
{`
.calendar-input-wrapper {
position: relative;
}
.calendar-clear-button {
position: absolute;
inset-inline-end: 6px;
inset-block-start: 27px;
}

.calendar-clear-button.with-icon {
inset-inline-end: 36px;
}
.calendar-clear-button.with-dense-wrapper {
inset-block-start: 23px;
}
`}
</style>
<style jsx>{`
.calendar-input-wrapper {
position: relative;
}
`}</style>
</>
)
}

CalendarInput.defaultProps = {
dataTest: 'dhis2-uiwidgets-calendar-inputfield',
}

CalendarInput.propTypes = {
...CalendarProps,
...InputFieldProps,
Expand Down
Loading
Loading